Komponenty Swing i architektura Model-View-Controller



Posługiwanie się bardziej złożonymi komponentami Swingu wymaga pojęcia o architekturze ich konstrukcji. Architektura ta nazywa się "Model-View-Controller" (MVC) i pełni bardzo ważną rolę w działaniu programów swingowych i w takim ich tworzeniu, by były elastyczne, uniwersalne i łatwo modyfikowalne. Ma również szersze znaczenie, jako ogólny wzorzec projektowy, stosowany np. w aplikacjach WEB. Tu zapoznamy się z ideą MVC na przykładzie komponentów Swingu, łatwo jednak będzie uogólnić to doświadczenie na inne zastosowania.

1. Modele i widoki

Swing bazuje na architekturze "Model-View-Controller" (MVC), przejętej (z modyfikacjami) z języka SmallTalk.

W architekturze tej:

Taka separacja umożliwia bardziej elastyczne i logiczne tworzenie kodu, m.in. dzięki skupieniu uwagi na modelu danych, który może następnie być prezentowany przez różne widoki.

W Swingu  połączono ze sobą widok i sterownik (View-Controller) w tzw. delegacie UI (od "user interface").  W ten sposób model danych jest odseparowany od komponentu (co umożliwia programowanie w kategoriach modelu), zaś zadania uwidaczniania i interakcji są przez komponent delegowane do odpowiedniego obiektu  UI (co daje  omawiany już wcześniej konfigurowalny wygląd - "pluggable look and feel").

Prawie wszystkie komponenty Swingu mają swoje modele, realizowane jako interfejsy.
Można wyróżnić dwa typy modeli:

 

 W tabeli pokazano modele dla komponentów Swingu.

Tabela. Modele komponentów Swingu

Komponent

Interfejs modelu

 Typ modelu

JButton

ButtonModel

GUI

JToggleButton

ButtonModel

GUI/dane

JCheckBox

ButtonModel

GUI/dane

JRadioButton

ButtonModel

GUI/dane

JMenu

ButtonModel

GUI

JMenuItem

ButtonModel

GUI

JCheckBoxMenuItem

ButtonModel

GUI/dane

JRadioButtonMenuItem

ButtonModel

GUI/dane

JComboBox

ComboBoxModel

dane

JProgressBar

BoundedRangeModel

GUI/dane

JScrollBar

BoundedRangeModel

GUI/dane

JSlider

BoundedRangeModel

GUI/dane

JTabbedPane

SingleSelectionModel

GUI

JList

ListModel

dane

JList

ListSelectionModel

GUI

JTable

TableModel

dane

JTable

TableColumnModel

GUI

JTree

TreeModel

dane

JTree

TreeSelectionModel

GUI

JEditorPane

Document

dane

JTextPane

Document

dane

JTextArea

Document

dane

JTextField

Document

dane

JPasswordField

Document

dane

Źródło: Amy Flower.  A Swing Architecture Overview.  The Inside Story on JFC Component Design, Swing Connection

Dostęp do modeli zapewniony jest poprzez metody getModel() i setModel(...) w klasach komponentów.

Za pomocą metody setModel(...) możemy ustalić własny model. Winien on być obiektem klasy implementującej odpowiedni  interfejs modelu.
Jeśli nie stworzymy własnego modelu, to każdy z komponentów będzie związany z pewnym modelem domyślnym, zrealizowanym jako klasa, której nazwa  zwykle zaczyna się słowem Default (np. DefaultListModel).
Po uzyskaniu dostępu do modelu (metodą getModel()) możemy wywoływać metody odpowiedniej klasy implementującej interfejs modelu.
W wielu przypadkach w klasach komponentów znajdują się metody, które pośredniczą w takich odwołaniach: są wygodne i pozwalają programować bez świadomości, iż korzysta się z modeli.
Np. w klasie JSlider mamy metody, które pozwalają uzyskać i ustalić aktualną wartość pokazywaną przez suwak oraz wartości maksymalne i minimalne. Faktycznie metody te  odwołują się do odpowiednich metod modelu typu BoundedRangeValue (np. getValue() z klasy JSlider jest zrealizowana jako getModel().getValue().

Jednak tylko przy prostych komponentach (np. JButton czy JSlider) lub przy prostych programach, korzystających z ograniczonego zakresu możliwości bardziej złożonych komponentów (np. lista, prezentująca stały zestaw wartości) można uniknąć świadomego użycia modeli, co zresztą jest dobrą praktyką programowania.
 

2. Lista: modele i widoki

Swingowa lista (komponent klasy JList) jest dobrym przykładem stopniowego przechodzenia od "bezmodelowego" programowania do programowania, w którym użycie modelu jest nie tylko potrzebne, ale i konieczne.

Lista jest związana z dwoma modelami:

Nie zawsze jednak musimy o tym wiedzieć.
Klasa JList ma dwa konstruktory, które nie odwołują się do pojęciu modelu i pozwalaja tworzyć listy bez świadomego użycia interfejsu ListModel (chociaż z niego korzystają);

Zatem najprostsza lista powstaje tak:

String[] napisy = { "Kot", "Pies", "Zebra" };

 JList list = new JList(napisy);

 JScrollPane sp = new JScrollPane(list); // konieczne by lista miała ew.
                                         // suwaki.
Po stworzeniu w ten sposób listy nie możemy do niej ani dodać ani z niej
usunąć żadnego elementu.

To statyczne zachowanie obowiązuje również w przypadku budowy listy z wektora (co może być zaskoczeniem, bowiem klasa Vector z natury oznacza dynamicznie rozszerzalną tablicę dowolnych obiektów; tymczasem lista nie odzwierciedla tych zmian - powody tego zachowania zostaną wyjaśnione później).

Mamy do dyspozycji wiele metod z klasy JList, które pozwalają na obsługiwanie statycznej listy np.:

Charakterystyczne, że wiele z metod klay JList replikuje odpowiednie metody z modelu GUI dla listy - ListSelectionModel (tak naprawdę odwołują się do odpowiednich metod  tego modelu).
Jest to uproszczenie życia, zwykły skrót: zamiast pisać list.getSelectionModel().getMinSelectionIndex() - piszemy list.getSelectedIndex(), nie wiedząc o tym, że ta ostatnia metoda "w środku" odwołuje się do modelu selekcji.

Również nasłuchiwanie zaznaczeń na liście może być realizowane przez przyłączenie do listy słuchacza zaznaczeń - obiektu implementującego interfejs ListSelectionListener z pakietu javax.swing.event.
Znowu uproszczenie życia:  ListSelectionListener tak naprawdę śledzi zmiany w modelu ListSelectionModel, który jest związany z daną listą.
Zamiast:
 
    list.getSelectionModel().addListSelectionListener(x);

możemy pisać:

     list.addListSelectionListener(x);

Uwaga: jeśli słuchacz jest  przyłączony do listy to źródło zdarzenia e typu ListSelectionEvent: e.getSource()  będzie samą listą. Natomiast w przypadku przyłączenia do modelu selekcji - źródłem będzie model.

O ile jednak bezpośrednie korzystanie z  modelu GUI (ListSelectionModel) lub jego zmiany są raczej niepotrzebne, to nieco bardziej elastyczne działania z listami wymagają posługiwania się modelem danych.

Co najmniej w dwóch przypadkach użycie modelu danych jest niezbędne:

Model danych dla listy jest określany przez interfejs ListModel, abstrakcyjna klasa AbstractListModel implementuje ten interfejs, pozostawiając do zdefiniowania w klasach pochodnych dwie ważne metody:


Object getElementAt(int i) - zwraca i-ty element modelu (element listy)
int getSize() - zwraca liczbę elementów listy

Własny model danych dla listy tworzymy dziedzicząc klasę AbstractListModel i dostarczając w niej definicji metod getElementAt i getSize oraz ew. dodatkowych metod służących do zmiany "zawartości" modelu.

Przykład: określmy model, który w sposób dynamiczny (bez uprzedniego zapisu w jakiejś tablicy) tworzy listę dat w styczniu.

AbstractListModel styczen = new AbstractListModel() {
      public Object getElementAt(int i) { return (i+1) + " stycznia"; }
      int getSize() { return 31; }
     }
Ustalenie modelu dla listy może odbyć się na dwa sposoby:

JList list = new JList(); // utworzenie listy z pustym modelem
list.setModel(styczen); // ustalenie modelu dla listy

albo:

JList list = new JList(styczen); // konstruktor z argumentem typu ListModel

Manipulacja modelami może być czasem użyteczna.

Rozważmy bardziej rozbudowany przykład listy-kalendarza z generalnym modelem kalendarzowym KListModel.

 class KListModel extends AbstractListModel {

  static Calendar kalend = new GregorianCalendar();

  static String[] nazwaDnia = {  "niedziela", "poniedziałek", "wtorek",
                                 "środa", "czwartek", "piątek", "sobota" };

  static String[] nazwaMies = { "stycznia", "lutego", "marca", "kwietnia",
                           "maja", "czerwca", "lipca", "sierpnia",
                           "września", "października", "listopada", "grudnia"
                         };
  static int[] ldni = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

  int rok;
  int mies;

  KListModel(int rok, int mies) {
     this.rok = rok;
     this.mies = mies - 1;
  }

  public Object getElementAt(int i) {
    kalend.set(rok, mies, i+1);
    int indDnia = kalend.get(Calendar.DAY_OF_WEEK) - 1;
    return (i+1) + "  "+ nazwaMies[mies] + " " + nazwaDnia[indDnia];
  }

  public int getSize() {
     return ldni[mies];
  }

}
r




Wykorzystamy ten model w innej klasie do prowadzenia "kalendarza".







public class List2 extends JFrame
             implements ActionListener {


  JList list;
  int rok;

  List2(int rok, int mies) {
    this.rok = rok;
    Container cp = getContentPane();
    list = new JList(new KListModel(rok, mies));
    cp.add(new JScrollPane(list), "Center");
    JPanel p = new JPanel(new GridLayout(2,0));
    p.setBorder(BorderFactory.createTitledBorder("Miesiące"));
    for (int i = 1; i <= 12; i++) {
      JButton b = new JButton(""+i);
      b.addActionListener(this);
      p.add(b);
      }
    cp.add(p, "South");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    setVisible(true);
  }

  public void actionPerformed(ActionEvent e) {
    int mies = Integer.parseInt(e.getActionCommand());
    list.setModel(new KListModel(rok, mies));
  }


  public static void main(String[] args) {
    JTextField rok = new JTextField(10);
    JTextField mies = new JTextField(10);
    rok.setBorder(BorderFactory.createTitledBorder("Rok"));
    mies.setBorder(BorderFactory.createTitledBorder("Miesiąc"));
    JOptionPane.showMessageDialog(null, new JTextField[] { rok, mies } );
    new List2(Integer.parseInt(rok.getText()),
              Integer.parseInt(mies.getText()));
   }

} 

 

Zauważmy: nadal nie możemy zmieniać zawartości listy.
To bowiem wymaga nie tylko metod dodawania nowych elementów i modyfikacji już istniejących (czego nasz model nie dostarczył), ale i komunikacji pomiędzy modelem a widokiem.


 

3. Zmiany w modelu i komunikacja model-widok


Są dwie generalne zasady:
  1. Wszelkie zmiany (które ew. będą uwidocznione w widoku) muszą być dokonywane jako zmiany w modelu danych.
  2. O każdej zmianie model musi powiadamiać zainteresowanych słuchaczy, kreując odpowiednie zdarzenia  za pomocą metod fire... Budowa klas Swingu gwarantuje, iż zdarzenia te będą obsłużone przez widoki (delegatów UI).

W przypadku listy możemy mieć trzy rodzaje modeli danych:


Np. przy tworzeniu listy z argumentem - wektorem wywoływany jest następujący konstruktor:

    public JList(final Vector v) {
        this (
            new AbstractListModel() {
                public int getSize() { return v.size(); }
                public Object getElementAt(int i) { return v.elementAt(i); }
            }
        );
    }

Tak stworzony "model automatyczny" nie pozwala uwidacznić na liście ew. dodawania lub usuwania elementów wektora, gdyż nie generuje żadnych zdarzeń, które mogłyby być przejęte przez słuchaczy modelu danych, wpływających na zmianę  widoku.

Należy więc zadbać o odpowiednie generowanie i dystrybucję zdarzeń zmian modelu we własnym modelu.

Zdarzenia zmian modelu należą do klasy ListDataEvent i mają następujące charakterystyki:


Do nasłuchu zmian w modelu służy interfejs ListDataListener, który udostępnia trzy metody:
    intervalAdded(ListDataEvent e)       // przedział dodany
    intervalRemoved(ListDataEvent e)   // przedział usunięty
    contentsChanged(ListDataEvent e)   // zmiana zawartości: np. wiele przedziałów zostało zmienionych

Klasa AbstractListModel definiuje trzy zabezpieczone metody, służące do powiadamiania słuchaczy o zmianach w modelu:


    protected void fireIntervalAdded(Object source, int index0, int index1)
    protected void fireIntervalRemoved(Object source, int index0, int index1)
    protected void fireContentsChanged(Object source, int index0, int index1),

które generują zdarzenia ListDataEvent z odpowiednimi charakterystykami i rozsyłają te zdarzenia do wszystkich zarejestrowanych słuchaczy.

Przykład:
model danych, który umożliwia dodawanie i usuwanie elementów.

class SimpleListModel extends AbstractListModel {

 Vector v = new Vector();   // elementy lista będą tu

 // domyślny konstruktor: do listy będziemy dodawać później, za pomocą metod add
 SimpleListModel() { }

 // inicjalna lista podana jako tablica obiektów
 SimpleListModel(Object[] o) {
   for (int i = 0; i<o.length; i++) v.addElement(o[i]);
}

 public int getSize() { return v.size(); }

 public Object getElementAt(int index) {
     return v.elementAt(index);
 }

// Dodaje element na pozycji index-1
 public void add(int index, Object o) {
  v.insertElementAt(o, index);
  fireIntervalAdded(this, index, index);
 }

// Dodaje element na końcu listy
 public void add(Object o) {
   add( getSize(), o);
 }

// Usuwa element na pozycji index
 void remove(int index) {
   v.removeElementAt(index);
   fireIntervalRemoved(this, index, index);
 }
}

 

Tworzenie listy za pomocą klasy SimpleListModel  może wyglądać tak:

String[] s = { "Obrazek1", "Obrazek2", "Obrazek3", ... "ObrazekN" };
SimpleListModel lm = new SimpleListModel(s);
JList  list = new JList(lm);

Testowanie modelu: dwa przyciski "Dodaj" i "Usuń" i obsługująca ich akcje metoda:

public void actionPerformed(ActionEvent e) {
   String s = e.getActionCommand();
   if (s.equals("Dodaj")) {
      String elem = "Dodatkowy element " + n++;
      int i = list.getSelectedIndex();
      if (i == 0 ) lm.add(i, elem);
       else lm.add(elem);
      }
   else {  // Usun
     int i = list.getSelectedIndex();
     if (i = 0) lm.remove(i);
     }
}
 

Zatem zawsze odwołujemy się do modelu.
Zawsze z modelu idą komunikaty do zainteresowanych słuchaczy ( a szczególnie do - automatycznie nasłuchującego zmiany modelu obiektu, który jest odpowiedzialny za zmiany widoku listy).

Do tworzenia standardowych dynamicznych list ( zmieniających swoje rozmiary ) nie trzeba wcale tworzyć własnego modelu danych.
Wystarczy wykorzystać klasę DefaultListModel z pakietu javax.swing.
Definiuje one model danych, zrealizowany jako wektor do którego elementy mogą być dodawane i z  którego elementy mogą być usuwane.

Robimy tak:

DefaultListModel lm = new DefaultListModel();
JList list = new JList(lm);

po czym manipulujemy modelem za pomocą (wielu!) metod klasy DefaultListModel (np. lm.addElement(...))
 

 

4. Nasłuch zmian w modelach: uogólnienie.

Obowiązuje podstawowa zasada: model powiadamia słuchaczy o zmianie swoich stanów.
W modelach domyślnych (tych zaczynających się od Default...) zostało to zagwarantowane.
We własnym modelu trzeba zadbać o odpowiednie wywoływanie metod fire...
Widoki zawsze nasłuchują. I - na skutek otrzymanych komunikatów - zmieniają swoje charakterystyki.
Ale my również możemy posługiwać się nasluchem modelu. I możemy podejmować własne akcje przy zmianach stanów modelu.

Istnieją dwa typy interfejsów nasłuchu:

 

Tabela. Nasłuch modeli

Model

Interfej nasłuchu

Klasa zdarzenia

BoundedRangeModel

ChangeListener

ChangeEvent

ButtonModel

ChangeListener

ChangeEvent

SingleSelectionModel

ChangeListener

ChangeEvent

ListModel

ListDataListener

ListDataEvent

ListSelectionModel

ListSelectionListener

ListSelectionEvent

ComboBoxModel

ListDataListener

ListDataEvent

TreeModel

TreeModelListener

TreeModelEvent

TreeSelectionModel

TreeSelectionListener

TreeSelectionEvent

TableModel

TableModelListener

TableModelEvent

TableColumnModel

TableColumnModelListener

TableColumnModelEvent

Document

DocumentListener

DocumentEvent

Document

UndoableEditListener

UndoableEditEvent

Uwaga: wszystkie powyższe interfejsy i klasy zdarzeniowe umieszczone są w pakiecie javax.swin.event. Źródło: Amy Flower.  A Swing Architecture Overview.  The Inside Story on JFC Component Design, Swing Connection



5. Suwaki: nasłuch zmian modelu za pomocą ChangeListenera

Suwak (komponent typu JSlider) służy do ustalania wartości z zadanego przedziału za pomocą przesuwania gałki (knob). Modelem danych/GUI dla suwaka  jest klasa implementująca interfejs BoundedRangeModel (domyślnie klasa DefaultBoundedRangeModel).

Model ten określa m.in. przedział wartości liczb całkowitych (maksymalna minimimalna) oraz aktualną wartość, znajdującą się w tym przedziale i zmieniającą się czy to na skutek przesuwania gałki suwaka czy też wywołania metody setValue().

Klasa JSlider praktycznie całkowicie ukrywa przed nami ten model, dostarcza bowiem własnych metod, które wołane na rzecz suwaka komunikują się z modelem, wykonując wszelkie potrzebne operacje.
Również nasłuch zmian modelu można prowadzić poprzez przyłączenie obiektu typu ChangeListener do samego suwaka.

Rozważmy przykład: nowy kalendarzowy program, który stanowi listę kalendarzową z możliwością wyboru lat i miesięcy.
Program korzysta z wprowadzonego wcześniej modelu danych dla listy KListModel, dodając do GUI dwa suwaki: jeden dla zmiany lat, drugi - miesięcy (zob. rysunek).

r


Główna klasa programu SliList implementuje interfejs ChangeListener. W klasie zdefiniowano jedyną metodę tego interfejsu - stateChanged(ChangeEvent).  W metodzie tej obsługiwane są zdarzenia zmiany aktualnej wartości obu suwaków (pobieranej metodą getValue() z klasy JSlider) i odpowiednio do tego ustalany jest nowy model danych dla listy.
Przy okazji tego programu zostaną przedstawione metody służące konfigurowaniu suwaka oraz niektóre metody związane z listą.
W szczególności warto zwrócić uwagę na:

Oto tekst programu.

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

public class SliList extends JFrame implements ChangeListener {

  JList list;                 // lista-kalendarz

  JSlider lata, miesiace;     // suwaki dla zmiany lat i miesiećy
  int rok, mies,              // rok i miesiąc pokazywany na liście
      brok, bmies, bdzien;    // bieżący rok, miesiąc, dzień

 SliList() {
  Container cp = getContentPane();

  Date teraz = new Date();    // określenie bieżącej daty
  brok = teraz.getYear()+1900;
  rok =  brok;
  bmies = teraz.getMonth()+1;
  mies = bmies;
  bdzien = teraz.getDate();

  // Tworzenie  i konfiguracja suwaka lat

  lata = new JSlider(JSlider.HORIZONTAL, 1990, 2010, rok);

  lata.setMajorTickSpacing(5);      // znakowanie skali
  lata.setMinorTickSpacing(1);
  lata.setPaintTicks(true);         //  znaczniki na skali widoczne
  lata.setPaintLabels(true);        // etykiety na skali widowczne

  lata.setBorder(BorderFactory.createTitledBorder("Lata"));

  lata.addChangeListener(this);     // słuchczem zmian będzie ten obiekt
  cp.add(lata, "North");

  // Tworzenie i konfiguracja suwaka miesięcu

  String[]  m = { "Styczeń", "Luty", "Marzec", "Kwiecień",
                  "Maj", "Czerwiec", "Lipiec", "Sierpień",
                  "Wrzesień", "Pażdziernik", "Listopad", "Grudzień"
                  };

  Icon icon = null;
  Hashtable labels = new Hashtable();     // asocjacyjna tablica obieków-etykiet
  for (int i = 1; i <= 12; i++) {
      icon = new ImageIcon(m[i-1]+".gif");  // ikona z obrazkiem z plik
      JLabel l = new JLabel(m[i-1], icon, JLabel.CENTER); // etykieta z ikoną
      if (i == bmies) l.setForeground(Color.red);  // kolor bieżącego miesiąca
      labels.put(new Integer(i), l);   // kluczem w tablicy jest pozycja suwaka,
                                       // wartośćią - obiekt-etykieta
      }

  miesiace = new JSlider(JSlider.VERTICAL, 1, 12, mies);
  miesiace.setLabelTable(labels); // ustalenie tablicy etykiet
  miesiace.setPaintLabels(true);
  miesiace.setMajorTickSpacing(1);
  miesiace.setPaintTicks(true);
  miesiace.setSnapToTicks(true);  // gałka dosuwana do najbliższego znacznika
  miesiace.setInverted(true);         // zamiana: minimalna wartość na górze
  miesiace.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
  miesiace.setPreferredSize(
   new Dimension(icon.getIconWidth() + 150, 12*(icon.getIconHeight()+10)));
  miesiace.addChangeListener(this);

  JPanel p = new JPanel(new BorderLayout());
  p.setBorder(BorderFactory.createLineBorder(Color.blue));
  p.add(miesiace, "East");

  list = new JList( new KListModel(rok, mies));

  // prototypowa wartość elementu listy zapewni odpowiednią szerokość:
  list.setPrototypeCellValue("31 pażdziernika poniedziałek");

  list.setSelectedIndex(bdzien-1);            // bieżący dzień zaznaczony
  list.ensureIndexIsVisible(bdzien-1);        // i na pewno widoczny

  p.add(new JScrollPane(list), "Center");

  cp.add(p, "Center");
  pack();
  setVisible(true);
}

// Obsługa wyboru  wartości na suwakach
public void stateChanged(ChangeEvent e) {
    JSlider sl = (JSlider) e.getSource();

    if (!sl.getValueIsAdjusting()) {               // gdy koniec przesuwania
       int n_rok = rok, n_mies = mies;
       if (sl == lata) n_rok = sl.getValue();      // pobranie wartości
        else if (sl == miesiace) n_mies = sl.getValue();

       // Jeżeli zmiana miesiąca i/lub roku:
       if (n_rok != rok || n_mies != mies) {
          rok = n_rok; mies = n_mies;
          list.setModel(new KListModel(rok, mies)); // nowy model
          if (rok == brok && mies == bmies) {  // jeżeli bieżący rok i miesiąc
            list.setSelectedIndex(bdzien-1);
            list.ensureIndexIsVisible(bdzien-1);
            }
            else list.ensureIndexIsVisible(0);
          }

       }
    }

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

}

 


6. Lista rozwijalna

Lista rozwijalna (JComboBox) stanowi połączenie pola tekstowego z listą
Pole tekstowe zawiera wybrany element listy oraz strzałkę służącą do rozwijania listy.
Domyślnie pole tekstowe jest nieedytowalne - wtedy "kombo" spełnia podobną rolę jak zwykła listy, a jego  walorem jest to, iż oszczędza miejsce (lista jest zamknięta, dopiero kliknięcie w strzałkę ją rozwija).
Jeśli pole tekstowe uczynimy edytowalnym (za pomocą odwołania setEditable(true)), możemy w nim wpisać dowolny tekst lub też wypełnić jego zawartość poprzez wybór elementu z listy rozwijalnej, otwieranej strzałką.
Wybór elementu listy rozwijlalnej jest obsługiwany przez obiekty typu ActionListener oraz ItemListener.

Klasa JComboBox ma dwie przyjemne właściwości:

To ostatnie nie oznacza jednak, że model danych jest nieużyteczny.

Wydruk i rysunek ilustrują omówione właściwości listy roziwjalnej.

r  

Wydruk

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

class Combo extends JFrame implements ActionListener {
	
	Container cp = getContentPane();
	
	public Combo() {
    cp.setLayout(new FlowLayout());

    String[] s = { "Warszawa", "Kraków", "Katowice",
                          "Gdańsk", "Szczecin" };

    final JComboBox combo = new JComboBox(s);
    combo.addActionListener(this);
    cp.add(combo);

    final JCheckBox check = new JCheckBox("Editable", false);
    check.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent e) {
          combo.setEditable(check.isSelected());

         }
     });

    cp.add(check);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    show();
    }


  // Obsługa akcji: gdy pole edytowalne i wprowadzono
  // nowy element - dodać go do listy

  public void actionPerformed(ActionEvent e) {
    JComboBox cb = (JComboBox) e.getSource();
    if (cb.isEditable()) {
      Object o = cb.getSelectedItem();
      int i = ((DefaultComboBoxModel) cb.getModel()).getIndexOf(o);
      if (i == -1) cb.addItem(o);
      }
    }

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

 

Jak widać, po to by uniknąć wprowadzenia duplikatu na listę konieczne było odwołanie do modelu danych - domyślnie obiektu klasy DefaultComboBoxModel.

Ogólnie, interfejs określający model danych dla "komba" rozszerza interfejs modelu danych zwykłej listy:

    public interface ComboBoxModel extends ListModel.

Rozwiązanie to przyjęto, gdyż lista rozwijalna może mieć tylke jedne wybrany element i nie miało sensu wprowadzać odrębnego modelu selekcji. Model selekcji jest więc zintegrowany z modelem danych poprzez dodanie do metod przejętych z interfejsu ListModel tylko dwóch metod: getSelectedItem i setSelectedItem.

Te wspólne korzenie umożliwiają łatwe tworzenie modeli danych, które mogą być prezentowane i jako zwykła lista i jako lista kombinowana.

Interfejs ComboBoxModel nie zawiera metod służących zmianie zawartości listy kombinowanej.
Możemy ich oczywiście dostarczyć przy jego implementacji w klasie opisującej nasz model danych.
Ale jeśli chcemy korzystać z metod addItem i insertItemAt z klasy JComboBox, to ustalony model danych dla listy kombinowanej musi implementować interfejs MutableComboBoxModel.
W domyślnym modelu (DefaultComboBoxModel) zostało to zagwarantowane.


7. Wykreślacze

Przy wykreślaniu komponentów, które zawierają "komórki" - jak np. lista (JList), lista rozwijalna (JComboBox), tabela (JTable) - używany jest specjalny obiekt, określający zasady wykreślania tych komórek.

Ten swoisty wykreślacz (renderer) jest zawsze obiektem klasy implementującej odpowiedni interfejs.

Np. "komórki" (elementy) listy oraz listy rozwijalnej domyślnie wykreślane są w oparciu o reguły dostarczane przez obiekt klasy DefaultListCellRenderer implementującej interfejs ListCelRenderer.
W tym przypadku komórki przedstawiane są jako etykiety (JLabel), które mogą zawierać tekst albo ikonę.

Zatem bez żadnych dodatkowych działań można stworzyć listę obrazków.
Jeżeli elementy listy nie są ani tekstami ani ikonami, to obiekty te przekształacane są w napisy za pomocą metody toString().
Ta reguła umożliwia łatwe określanie sposobu prezentacji na liście złożonych obiektów (wystarczy w ich klasach zdefiniować odpowiednio metodę toString()).

Jednak podobnie jak modele, również wykreślacze możemy budować po swojemu, umożliwiając np. tworzenie list z elementami, które wyglądają niemal dowolnie.
Nawet pełniejsze wykorzystanie możliwości standardowej  klasy JLabel (jako komponentu na którym wykreślane są komórki list) jest wielce interesujące.

Interfejs ListCellRenderer (używany dla listy zwykłej i kombinowanej) ma jedną metodę z podanymi argumentami:

 public Component getListCellRendererComponent(
     JList list,                       // lista (komponent na którym odbywa się wykreślanie)
     Object value,                 // wartość zapisana w komórce (elemencie)
     int index,                       // indeks komórki (elementu)
     boolean isSelected,        // czy komórka jest zaznaczona
     boolean cellHasFocus)   // czy ta komórka ma fokus

Metoda ta zwraca komponent, którego metoda paint(..) ma odpowiadać za wykreślanie komórki.

Kiedy jakaś komórka ma pojawić się na ekranie, odpowiedni obiekt odpowiedzialny za wyświetlanie listy (np. delegat UI dla klasy JList - BasicListUI z pakietu javax.swing.plaf.basic) pobiera wykreślacz (metoda getCellRenderer z klasy JList) i dowiaduje się od niego jaki komponent ma wykreślić komórkę ( getListCellRendererComponent), po czym zleca wykreślenie tego właśnie komponentu.

Aby stworzyć własny wykreślacz dla listy należy zdefiniować klasę implementującą interfejs wykreślania (w tym przypadku ListCellRenderer) i zdefiniować w niej metodę getListCellRendererComponent. Następnie należy ustalić nowy wykreślacz dla listy metodą setCellRenderer z klasy JList.

Te zasady obowiązują (z dokładnością do nazw) wszystkie klasy "komórkowe", również JTree i JTable.

r Rozważmy przykład: lista kalendarzowa (oparta na omawianym wcześniej modelu KListModel), której komórki są wyświetlane w następujący sposób:

 
Za komponent odpowiedzialny za wykreślanie komórki wystarczy JLabel. Zatem klasa wykreślacza dziedziczy JLabel.

Wydruk tekstu programu:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;


class CalListCellRenderer extends JLabel implements ListCellRenderer {

  Map icons;                     // ikony oznaczające specjalne daty
  Icon defaultIcon;              // ikona domyślna

  public CalListCellRenderer(Map icons, Icon deflt) {
   this.icons = icons;
   defaultIcon = deflt;
   setOpaque(true);  // konieczne dla zmiany tła etykiety
  }

 public Component getListCellRendererComponent(
                               JList list, Object value,
                               int index, boolean isSelected,
                               boolean cellHasFocus)
 {
     String napis = (String) value;
     setText(napis);
     Icon icon = (Icon) icons.get(new Integer(index));
     if (icon == null) icon = defaultIcon;
     setIcon(icon);
     Color bc = new Color(211, 211, 211);
     if (napis.endsWith("niedziela")) {    // niedziele
        setBackground(isSelected ? Color.red : bc);
        setForeground(isSelected ? bc : Color.red);
        setFont(new Font("Dialog", Font.BOLD, 18));
        }
      else {
       setBackground(isSelected ? new Color(165,191,221) : bc);
       setForeground(Color.black);
       setFont(new Font("Dialog", Font.BOLD, 12));
       }
     return this;
 }
}


public class TestCLR extends JFrame {

  JList list;
  int rok, mies, brok, bmies, bdzien;
  Map tips = new HashMap();

  TestCLR() {
    Date teraz = new Date();

    brok = teraz.getYear()+1900; // bieżący rok
    rok =  brok;
    bmies = teraz.getMonth()+1;  // bieżacy miesiąc
    mies = bmies;
    bdzien = teraz.getDate();    // bieżacy dzień


    Container cp = getContentPane();

    Icon icon[] = { new ImageIcon("blarr.gif"),
                    new ImageIcon("larr.gif"),
                  };
    // specjalna ikona dla:
    tips.put(new Integer(2), icon[1]);  // 3 dzień miesiąca
    tips.put(new Integer(5), icon[1]);  // 6 dzień miesiąca

    list = new JList( new KListModel(brok, bmies));
    list.setCellRenderer(new CalListCellRenderer(tips, icon[0]));

    list.setSelectedIndex(bdzien-1);
    list.ensureIndexIsVisible(bdzien-1);
    cp.add(new JScrollPane(list));
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    setVisible(true);
  }

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

}

 

UWAGA!

W klasie wykreślacza możemy dziedziczyć dowolne komponenty (np. przyciski czy wielopola edycyjne).
Ale oznacza to TYLKO TYLE, że komórki będą miały wygląd dziedziczonych komponentów.
Za pomocą wykreślaczy nie uzyskamy FUNKCJONALNOŚCI dziedziczonych komponentów tj. np. przycisk na liście nie jest prawdziwy (tylko wygląda jak przycisk), pole edycyjne wcale nie jest "żywym" polem edycyjnym - tylko wygląda jak pole.
Jedynie wtedy, gdy dziedziczony komponent ma właściwość zaznaczony - nie zaznaczony,  możemy w wykreśalaczu symulować jego funkcjonalność (łatwo np. stworzyć listę znaczników - obiektów typu JCheckBox)

 


8. Tabele

Tabele są obiektami klasy JTable. Jest to jeden z najbardziej rozbudowanych komponentów Swingu. Dokładne omówienie tabel zajęłoby bardzo wiele miejsca.
Z tego względu zostanie tu przedstawiona ogólna orientacja, akcentująca modele danych, wykreślacze i edytory komórek.
Inne właściwości tabel (np. rodzaje selekcji wierszy i kolumn, zmiany rozmiarów komórek, polityki wymiarowania) można w łatwy sposób poznać na podstawie specyfikacji Java API .

Proste tabele tworzy się bardzo łatwo: wystarczy użyć jednego z dwóch konstruktorów, które bezpośrednio akceptują dane tabeli, np.

Object[][] dane = { {"Kowalski", "Jan", new Integer(30), new Boolean(true) },
                    { Jankowski", "Jan", new Integer(20), new Boolean(false) },
                    ....
                  };
Object[] nazwyKolumn = {"Nazwisko", "Imię", "Wiek", "Urlop wykorzystany?" }'

JTable tab = new JTable(dane, nazwyKolumn);
JScrollPane sp = new JScrollPane(tab); // aby były suwaki i nagłówki kolumn!

Drugi "prosty" konstruktor jako dane ma wektor wektorów, określający wiersze tabeli:

    JTable(Vector wiersze, Vector nazwyKolumn);

Elementy tabeli mogą być dowolnymi obiektami. Mogą też podlegać edycji.
Szerokość kolumn może być zmieniana za pomocą myszki, kolumny mogą też być przestawiane myszką. Takie są domyślne właściwości tabeli (które zresztą można zmienić).
W klasie JTable znajduje się  wiele metod określających wygląd i zachowanie tabeli.

Proste tabele tworzone bezpośrednio z tablic lub wektorów obiektów są niezbyt elastyczne bowiem:

Potrzebne jest więc "wyjście" poza klasę JTable i świadome użycie modeli.

Każda tabela jest związana z trzema modelami:

Dane tabeli są definiowane przez obiekt klasy implementującej interfejs TableModel.
Implementacja określa w jaki sposób dane są przechowywane i/lub generowane oraz dostarcza metod pobierania i ustalania wartości elementów modelu. Ponadto w modelu danych określamy, które elementy modelu mogą być w tablicy edytowane.

Zakłada się (abstrakcyjnie), że elementy modelu są umieszczone w wierszach i kolumnach, mają zatem dwa indeksy.  Położenie kolumny w ramach modelu może różnić się od położenia odpowiadającej jej kolumny tabeli w widoku JTable (np. jeśli w widoku przestawimy kolumny). Nie musimy się tym jednak martwić: o odpowiednią synchronizację dba obiekt implementujący interfejs  TableColumnModel.

TableColumnModel zarządza kolumnami tabeli - obiektami typu TableColumn.
Tabela jest więc zestawem kolumn - obiektów typu TableColumn.
Możemy się dowiedzieć jakie są kolumny w tabeli pytając o to model kolumn:
        getColumns()
lub używając (jako łatwiejszej drogi) tej samej metody z klasy JTable.
Możemy też dodawać, usuwać, programistycznie przestawiać kolumny wywołując odpowiednie metody  modelu kolumn lub ich odpowiedniki z klasy JTable.

Klasa TableColumn zawiera metody określające właściwości kolumn tabeli.
Z każdą kolumną - obiektem TableColumn - skojarzony jest:


Wykreślacz komórek (tak jak w przypadku JListy) określa zasady wykreślania komórek i zwraca komponent, który jest odpowiedzialny za wykreślenie komórki. Komórka ma jedynie wygląd danego komponentu, bez jego funkcjonalności.

Edytory komórek są natomiast w pełni interaktywne: definiują komponenty, które służą do edycji komórek i komponenty te mają swoją normalną funcjonalność.

Dla każdej kolumny możemy ustalić specyficzny wykreślacz i specyficzny edytor komórek.
Wtedy określone komponenty będą używane do wykreślania i edytowania komórek danej kolumny.
Jeśli nie określono specyficznych wykreślaczy i edytorów - używane są wykreślacze i edytory, bazujące na typie danych zawartych w kolumnie. Pewne typy danych mają ustalone domyślne wykreślacze i edytory (zob. tabela)

Tabela. Domyślne wykreślacze komórek

Typ danych

Wykreślacz

Object

JLabel z tekstem powstałym po zastosowaniu wobec obiektu metody toString()

Boolean

JCheckBox

Number

JLabel z wyrównanym do prawej Stringiem reprezentującym liczbę 

Icon

JLabel z wycentrowaną ikoną


Sposób doboru domyślnych edytorów jest podobny.

Ogólnie, elementy modelu danych są typu Object: definiując model możemy określić bardziej konkretne typy danych dla konkretnych kolumn (służy temu metoda getColumnClass, zwracająca klasę podanej kolumny). W ten sposób możemy korzystać z domyślnych wykreślaczy i edytorów.
Sami również możemy ustalić wyświetlacz i edytor komórek dla konkretnego typu danych (np. dla wszystkich kolumn o wartościch typu Color).

Uwaga: jeśli dla jakiejś kolumny ustalono specyficzny wykreślacz i/lub edytor, to ma on pierwszeństwo wobec wykreślacza/edytora bazującego na typie danych.
 
Omówione skrótowo koncepcje szczegółowo przedstawi rozbudowany przykład.

Tworzymy tablicę, która winna zawierać kalendarz dla podanego miesiąca roku  z kolumnami: "Dzień", "Pogoda" (opis pogody: słońce, deszcz itp.), "Temperatura", "Czy dzień udany?". Kolumna "Pogoda" winna zawierać napis, przy czym edycja komórki sprowadza się do wyboru z kilku możliwych napisów (edytorem będzie więc JComboBox).
Komórki kolumny "Temperatura" wyświetlać będziemy w postaci etykiet, których tło będzie się zmieniać w zależności od temperatury, natomiast eydtor komórek będzie suwakiem. Wreszczie pytanie o udany dzień - w sposób naturalny prezentuje się jako znacznik (JCheckBiox).

Wygląd tabeli przedstawia rysunek.
r  

Zaczniemy od modelu danych.  Klasa modelu implementuje interfejs TableModel poprzez odziedziczenie gotowej klasy AbstractTableModel i dostarczenie definicji kilku ważnych metod:

public int getColumnCount()     // ile kolumn w tablicy
public int getRowCount()          // ile wierszy
public String getColumnName(int col) // nagłówek kolumny
public Class getColumnClass(int col)   // klasa danych w kolumnie
public boolean isCellEditable(int row, int col)  // czy element edytowalny
public Object getValueAt(int i, int j)    // wartość elementu (i,j)
public void setValueAt(Object value, int i, int j)  // ustalenie wartości
 

Bardzo łatwo określimy pięć pierwszych metod (wydruk 1):
class KalTableModel extends AbstractTableModel {
  ....
  static String[] columnNames = { "Dzień", "Pogoda",
                                  "Temperatura", "Czy był udany?"
                                };

  public int getColumnCount() { return columnNames.length; }

  public int getRowCount() {
   //  zwraca liczbę dni w danym miesiącu
  }

  public String getColumnName(int col) {
    return columnNames[col];
  }

  public Class getColumnClass(int c) {
    // klasa danych w pierwszej komórce kolumny c
    return getValueAt(0, c).getClass();
  }

  public boolean isCellEditable(int row, int col) {
    if (col == 0) {  // komórki kolumny "Dzień" nie są edytowalne!
      return false;
    } else {
        return true;
    }
  }
  //....
} 
Serce modelu danych stanowi znany nam już z przykładu JListy model kalendarzowy, uzupełnieniony o tablice przechowujące wartości  danych dla typu pogody w danym dniu,  temperatury oraz informację czy dzień był udany (tak - nie). Na tej podstawie definiujemy metody getValueAt i setValueAt. Dodatkowo zdefiniowaliśmy publiczne pola z minimalną i maksymalną temperaturą, które będą używane przy ustawianiu suwaka w wykreślaczu i edytorze, oraz metodę getTypPogody() zwracającą tablicę napisów z możliwym,i typami pogody, którą wykorzystamy przy definiowaniu edytora typu JComboBox. Dokończenie klasy KalTableModel przedstawia wydruk 2.
 

Wydruk 2

class KalTableModel extends AbstractTableModel {

  //...

  final static Calendar kalend = new GregorianCalendar();
  final static String[] nazwaDnia = {  "niedziela", "poniedziałek", ... };
  final static String[] nazwaMies = {  "stycznia", "lutego", ...   };
  final static String[] typPogody = {"Nieznana", "Słońce", "Chmury",
                                     "Deszcz", "Śnieg"
                                    };
  public final static int minTemp[] = { -50, -50, -40, -20, -10,
                                          0, 0, 0, -10, -20, -40, -40 };
  public final static int maxTemp[] = { 15, 15, 25, 30, 40,
                                        40, 50, 50, 50, 40, 30, 20, 20 };

  int mies;                            // miesiąc
  Integer[] temper = new Integer[31];  // temperatura
  String[] pogoda = new String[31];    // opis pogody

  Boolean[] udany = new Boolean[31];   // czy dzień udany?

  KalTableModel(int mies) {           // numer miesiąca
     this.mies = mies - 1;
     kalend.set(2000, mies, 1);       // ustawienie kalendarza
     for (int i = 0; i < 31; i++) {   // inicjalizacja innych pól
        temper[i] = new Integer(minTemp[mies-1]);
        udany[i] = new Boolean(false);
        pogoda[i] = "Nieznana";
        }
  }

 public String[] getTypPogody() { return typPogody; }
 // ....

 public Object getValueAt(int i, int j) {
     Object o = null;
     kalend.set(Calendar.DAY_OF_MONTH, i+1);
     int indDnia = kalend.get(Calendar.DAY_OF_WEEK) - 1;
     switch (j) {
       case 0 :  o = (i+1) + "  "+ nazwaMies[mies] + " " + nazwaDnia[indDnia];
                     break;
       case 1 :  o = pogoda[i]; break;
       case 2 :  o = temper[i]; break;
       case 3 :  o = udany[i]; break;
       default: break;
       }
     return o;
    }

 public void setValueAt(Object value, int i, int j) {
     switch (j) {
       case 1 : pogoda[i] = (String) value;  break;
       case 2 : temper[i] = (Integer) value; break;
       case 3 : udany[i] = (Boolean) value; break;
       default: break;
       }
     // zmiana modelu: należy powiadomić wszystkich TableModelListener-ów
     fireTableCellUpdated(i, j); // komórka zmodyfikowana
 }

//...

} 
Tworząc tabelę jako argument konstruktora podamy nasz model:

      JTable tabkal = new JTable(new KalTableModel(6));  // kalendarz dla czerwca

W tej chwili ustalone są domyślne wykreślacze i edytory komórek.
W szczególności, wartośći w kolumnie "Czy dzień był udany?"  (w modely typ Boolean) są pokazywane i edytowane jako JCheckBox (dzięki temu, że w modelu getColumnClass dla tej kolumny zwraca klasę Boolean).
To nam odpowiada. Pozostawimy też domyślny rendering komórek kolumny "Dzień" (napisy na etykietach).
Musimy natomiast zmienić sposób edycji komórek kolumny "Pogoda" (ma to być wybór z listy rozwijalnej) oraz wyreślanie i edycję komórek "Temperatura".

Pierwsze zadanie jest bardzo łatwe do wykonania.
Domyślny edytor komórek zdefiniowany przez klasę DefaultCellEditor pozwala na edycję za pomocą listy rozwijalnej.
Ma on bowiem trzy konstruktory z argumentami określającymi typ komponentu edytora: JTextField albo JCheckBox albo właśnie JComboBox.

Trzeba więc stworzyć odpowiednią listę rozwijalną, podać ją w kosntruktorze DefaultCellEditor, po czym nowy obiekt-edytor ustalić dla kolumny "Pogoda":

  KalTableModel tm = (KalTableModel) tabkal.getModel();
  String[] s = tm.getTypPogody();
  JComboBox cbox = new JComboBox();
  for (int i=0; i<s.length; i++) cbox.addItem(s[i]);
  tabkal.getColumn("Pogoda").setCellEditor(new DefaultCellEditor(cbox));

Zwróćmy uwagę: metoda setCellEditor pochodzi z klasy TableColumn.
Obiekt-kolumnę uzyskujemy poprzez odwołanie getColumn(Object id), gdzie id - jest identyfikatorem kolumny (tu domyślnie = nazwie kolumny).

Aby zapewnić wykreślanie komórek typu Integer w postaci różnobarwnych etykiet i edycję tych komórek  za pomocą suwaka musimy napisać własne klasy implementujące TableCellRenderer i CellEditor.

Podobnie jak w przypadku listy, wykreślacz wymaga implementacji tylko jednej metody, która zwraca komponent, odpowiedzialny za wykreślanie komórki:

   public Component getTableCellRendererComponent(

              JTable table,             // tabela
              Object value,            // wartość komórki
              boolean isSelected,   // czy zaznaczona
              boolean hasFocus,    // czy ma fokus (ostatnio kliknięta)
              int row, int column   // indeksy wiersza i kolumny komórki

              )


Klasę wykreślacza pokazano na wydruku 3:
class TempCellRenderer implements TableCellRenderer {

   JPanel p = new JPanel();
   JLabel l = new JLabel();

   TempCellRenderer(int mies) {
     l.setFont(new Font("Dialog", Font.BOLD, 18));
     l.setBorder(BorderFactory.createLineBorder(Color.black));
     l.setText("?");
     l.setOpaque(true);
     l.setBackground(Color.yellow);
     p.add(l);
     }

   public Component getTableCellRendererComponent(JTable table, Object value,
                    boolean isSelected, boolean hasFocus, int row, int column) {
     if (isSelected) {
       p.setForeground(table.getSelectionForeground());
       p.setBackground(table.getSelectionBackground());
     } else{
       p.setForeground(table.getForeground());
       p.setBackground(table.getBackground());
     }
     if (value != null)  {
       int val = ((Integer) value).intValue();
       if (val > 25)  {
         l.setBackground(Color.red);
         l.setForeground(Color.yellow);
       }
       else if (val < 10)  {
         l.setBackground(Color.blue);
         l.setForeground(Color.white);
       }
       else  {
         l.setBackground(Color.yellow);
         l.setForeground(Color.black);
       }

       l.setText(""+ value);
     }
     else l.setText("?");
     return p;
   }
}

 

 
Zdefiniowanie edytora jest nieco trudniejsze. Interfejs TableCellEditor zawiera wiele metod, które trzeba zdefiniować (głównie przejętych z interfejsu CellEditor).
Można sobie ułatwić życie dziedzicząc klasę DefaultCellEditor (gdzie wiele metod interfesju zostało zaimplementowane).

Musimy zdefiniować tylko kilka metod:

 public Component getTableCellEditorComponent(
      JTable table,                  // tabela
      Object value,                 // wartość komórki
      boolean isSelected,        // czy zaznaczona
      int row,    int column)   // indeks wiersza i kolumny (w modelu)
 

   public Object getCellEditorValue()
 

Implementację pokazuje wydruk 4.

Wydruk 4

class TempCellEditor extends DefaultCellEditor {

  JPanel p = new JPanel(new BorderLayout());
  JSlider slider;
  int slVal;  // wartość na suwaku

  TempCellEditor(int mies) {  // argument: numer miesiąca

    // trzeba wybrać jeden z trzech konstruktorów nadklasy!
    // DefaultCellEditor z JCheckBox otwiera edycję po jednym kliknięciu

    super(new JCheckBox());

    slider = new JSlider(JSlider.HORIZONTAL,
             KalTableModel.minTemp[mies-1],
             KalTableModel.maxTemp[mies-1],
             KalTableModel.minTemp[mies-1]);

    slider.setMajorTickSpacing(10);
    slider.setMinorTickSpacing(1);
    slider.setPaintTicks(true);
    slider.setPaintLabels(true);

    // Obsługa zmian położenia suwaka

    slider.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
         if (!slider.getValueIsAdjusting()) { // gdy przesuwanie zakończone
             slVal = slider.getValue();       // pobranie wartości
             fireEditingStopped();            // koniec edycji
             }
      }
     });
    p.add(slider);
    }

  public Component getTableCellEditorComponent(JTable table, Object value,
                    boolean isSelected, int row, int column) {

    if (isSelected) {  // kolory
      p.setForeground(table.getSelectionForeground());
      slider.setForeground(table.getSelectionForeground());
      p.setBackground(table.getSelectionBackground());
      slider.setBackground(table.getSelectionBackground());
    }
    else {
      slider.setForeground(Color.black);
      slider.setBackground(table.getBackground());
      p.setBackground(table.getBackground());
    }

    slVal = ((Integer) value).intValue();  // ustawienie wartości na suwaku
    slider.setValue(slVal);
    return p;
    }

    public Object getCellEditorValue() {
      return new Integer(slVal);  // zwracamy aktualną wartość suwaka
    }

    protected void fireEditingStopped() {
      super.fireEditingStopped();   // wywołanie metody z nadklasy
    }
}
Zdefiniowane wykreślacz i edytor komórek możemy ustalić jako domyślne dla edycji kolumn z komórkami typu Integ er (setDefaultCellRenderer(Integer.class, new SliderRenderer(mies)) i setDefaultCellEditor(Integer.class, new TempCellEditor(mies));

Tu jednak potraktujemy te obiekty jako wyłącznie do obsługi kolumny "Temperatura":

  TableColumn col =  tabkal.getColumn("Temperatura");
  col.setCellEditor(new TempCellEditor(6));
  col.setCellRenderer(new SliderRenderer(6));
  col.setMinWidth(150);
 
Tabele mogą służyć nie tylko do przeglądania i edycji informacji (zawartej w modelu danych), ale również stanowić wysublimowane interfejsy, reagującee na różnego rodzaju zdarzenia.

Zmiany modelu danych powodują zdarzenia typu TableModelEvent, które mogą być obsługiwane przez obiekty klas implementujących interfejs TableModelListener.
Słuchacze zmian modelu winni być przyłączani do modelu :

    tablica.getTableModel().addTableModelListener(....).

Zmiany w modelu kolumn powodują zdarzenia typu TableColumnModelEvent, obsługiwane przez słuchaczy przyłączanych do TableColumnModel.
Wszelkie zmiany w modelu kolumn mogą być też obsługiwane przez obiekty typu ChangeListener, a zmiany selekcji - nasłuchiwane  przez słuchaczy typu ListSelectionListener.

Oprócz tego klasa JTable - jako pochodna od JComponent - umożliwia rejestrację wszelkich słuchaczy, własciwych dla JComponent (np. MouseListenera czy KeyListenera).

 

9. Drzewa

Klasa JTree zapewnia hierarchiczne obrazowanie danych w postaci drzewa.

r Elementy danych są wyświetlane pionow i w widoku drzewa nazywane  węzłami (node) - zob. rysunek, przy czym:
Węzły są obiektami klas implementujących interfejs TreeNode. Zazwyczaj dla tworzenia węzłów będziemy używali standardowej, domyślnej klasy, która implementuje ten interfejs - DefaultMutableTreeNode. Argumentem konstruktora tej klasy jest element danych (typ Object), który ma być pokazany jako węzeł dzrzewa.

W drzewach - podobnie jak w tabelach - mamy do czynienia:
Łatwo będzie - choćby  przez analogię do list i tabel - operować tymi składnikami, korzystając z pomocy w postaci dokumentacji Java API.

Teraz zobaczymy tylko w jaki sposób można tworzyć drzewa, korzystając z klasy DefaultMutableNode, pozwalającej na tworzenie i dodawanie węzłów oraz jak obsługiwac selekcję węzłów terminalnych przez użytkownika. Kod pokazany na  wydruku zawiera niezbędne komentarze.
import java.awt.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;


public class Tree extends JFrame
                  implements TreeSelectionListener {


 String rodzaje[] = { "Owoce", "Kwiaty", "Zwierzaki" };
 String dane[][] =  { { "Jabłka", "Gruszki", "Winogrona", "Mandarynki" },
                      { "Frezje", "Lilie", "Róże", "Bez", "Gerbery" },
                      { "Pies", "Kot", "Krowa", "Koń", "Byk", "Owca" }
                    };
 public Tree() {
  // rdzeń hierarchii  - zmienna pocz
  DefaultMutableTreeNode pocz = new DefaultMutableTreeNode("Rodzaje");
  DefaultMutableTreeNode tn = null;
  for (int i=0; i < rodzaje.length; i++) {  // tworzymy węzły
      tn = new DefaultMutableTreeNode(rodzaje[i]);
      pocz.add(tn);
      for (int j=0; j<dane[i].length; j++)  // elementy treminalne ("liście")
         tn.add(new DefaultMutableTreeNode(dane[i][j]));
      }

  JTree tree = new JTree(pocz);           // drzewo wyrasta z pnia pocz!
  tree.addTreeSelectionListener(this);    // nasłuch selekcji
  JScrollPane sp = new JScrollPane(tree);
  getContentPane().add(sp);
  pack();
  setVisible(true);
 }

 // Implementacja metody interfejsu  TreeSelectionListener
 public void valueChanged(TreeSelectionEvent e) {
   JTree tree = (JTree) e.getSource();
   DefaultMutableTreeNode node = (DefaultMutableTreeNode)
       tree.getLastSelectedPathComponent();  // węzeł
   if (node == null) return;
   Object value = node.getUserObject();  // co zawiera
   // jeśli to liść (terminalny element) - wypisujemy info
   if (node.isLeaf())  System.out.println(value);
 }

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

}
 

10. Komponenty tekstowe

Najbardziej złożone w Swingu są  komponenty tekstowe.
Wiele rozbudowanych klas i interfejsów pozwala w dość prosty sposób tworzyć najbardziej wyszukane procesory tekstu. Cała koncepcja charakteryzuje się pełną otwartością: tekstowe klasy Swingu są przygotowane na przetwarzanie dowolnych dokumentów z dowolnymi strukturami elementów, które to elementy mogą charakteryzować się dowolnymi atrybutami.
Szczegółowe omówienie tematyki komponentów tekstowych znacznie wykracza poz ramy tej wykładu.
Tu chodzi nam o wstępną orientację w pojęciach związanych z kompnentami tekstowymi.
Hierarchię klas komponentów tekstowych przedstawia rysunek.

r
 
JTextArea i JTextField są odpowiednikami znanych nam z AWT komponentów tekstowych (ale mają znacznie rozbudowane możliwości). JPasswordField  jest polem tekstowym automatycznie maskującym wprowadzane hasło. Wszystkie te trzy komponenty mogą obsługiwać tylko czysty tekst (Unikod).

JEditorPane i JTextPane obsługują dokumenty złożone (z różnorodnymi stylami fragmentów tekstu, mogące zawierać inne niż tekst obiekty np. obrazki i nawet interaktywne komponenty Swingu).
JEditorPane obsługuje: czysty tekst oraz formaty HTML i RTF.
JTextPane pozwala tworzyć i redagować dowolne dokumenty złożone.
 
Wszystkie komponenty tekstowe (nawet te najprostsze) opierają się na bardzo rozbudowanej architekturze w pełni realizującej koncepcję MVC.  Wyróżniane są następujące (odseparowane) części: Klasa bazowa JTextComponent integruje funkcjonalność wielu z w/w odseparowanych części. Tam powinniśmy szukać podstawowych metod do obsługi dokumentów.

Komponentowe klasy pochodne uzupełniają te metody, szczególnie znacząco w przypadku komponentów przeznaczonych do obsługi złożonych dokumentów.

Jeśli jakichś metod nam brakuje - winniśmy sięgać do klas definiujących modele danych i kontrolery (edytory).

Jeśli chcemy tworzyć nowe rodzaje dokumentów i nowe sposoby ich obsługi musimy zadbać o odpowiednie implementacje/dziedziczenie interfejsów/klas typu Document (AbstractDocument) i EditorKit.

Swing dostarcza domyślnych klas obsługujących modele danych dokumentów, klas realizujących edytory, kursory tekstowe i podświetlacze.  Jednocześnie komponenty tekstowe domyślnie posługują się określonymi modelami dokumentów i edytorami.
Np. JTextArea ma jako model dokumentu klasę PlainDocument i używa jako edytora DefaultEditorKit, natomiast JTextPane domyślnie używa domyślnie DefaultStyledDocument oraz StyledEditorKit.

Dla komponentów obsługujących dokumenty złożone możemy rejestrować wiele edytorów (odpowiadających różnym modelom dokumentów). Wybór edytora następuje wtedy na podstawie formatu dokumentu.

Z komponentami tekstowymi wiążą się również nowe zdarzenia i nowe interfejsy nasłuchu:
r Niektóre z w/w cech komponentów tekstowych poznamy bliżej na prostym przykładzie wielopola edycyjnego (JTextArea), które wyposażymy w menu akcji edycyjnych (w tym takich jak undo, redo oraz zapamiętywanie i odtwarzanie pozycji w tekście - set bookmark, go to bookmark), nowozdefiniowane klucze (dla operacji schowkowych - zgodne z CUA - Ctrl-Insert, Shuft-Insert; dla operacji cofania i przywracania, dla operacji ustalania i odtwarzania pozycji) oraz etykietę stanu, która będzie pokazywać bieżący wiersz i kolumnę położenia kursora tekstowego (rysunek) .
 

 
 
 
Zacznijmy od akcji standardowych.

Cel pierwszy: umieścić w menu akcje kopiowania, wycinania i wklejania tekstu do/ze schowka oraz
przypisać akcje "schowkowe" standardowym klawiszom Ctrl-Insert, Shift-Delete, Shift-Insert.

Domyślny edytor - DefaultEditorKit - dzieli pomiędzy wszystkimi komponentami tekstowymi określone standardowe akcje edycyjne (w tym działania z systemowym schowkiem).
Akcje te są zdefiniowane jako obiekty typu Action, a każdy z tych obiektów zawiera nazwę akcji.

Akcje zdefiniowane przez DefaultEditorKit możemy pobrać z mapy akcji komponentu tekstowego, posługując się kluczami - nazwami akcji (nazwy standardowych akcji edycycjnych są zdefiniowane jako stałe statyczne typu String w klasie DefaultEditorKit).
Do przypisania akcji klawiszom wykorzystamy własną metodę setKey(..), korzystającą oczywiście z ogólnego mechanizmu powiązań InputMap i ActionMap (ta metoda nie jest tu niezbędna, ale przyda się nam dalej).

Rozwiązanie postawionego problemu ptrzedstawia poniższy fragment.

Fragment 1.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import java.util.*;

public class Edytor extends JFrame {

 JTextArea txtArea = new JTextArea();
 // ...

public Edytor() {

 // ...
 JMenu eMenu = new JMenu("Edit");
 // ...

 ActionMap taaMap = txtArea.getActionMap();
 eMenu.add(taaMap.get("copy-to-clipboard")).setText("Copy");
 eMenu.add(taaMap.get("cut-to-clipboard")).setText("Cut");
 eMenu.add(taaMap.get("paste-from-clipboard")).setText("Paste");
 eMenu.addSeparator();

 setKey("copy-to-clipboard", "control INSERT");
 setKey("cut-to-clipboard", "shift DELETE");
 setKey("paste-from-clipboard", "shift INSERT");
 // ...

}

// ...

void setKey(Object action, String stringKey) {
  String actionName = null;
  ActionMap amap = txtArea.getActionMap();
  if (action instanceof String)   {
      if (amap.get(action) == null) return;
      actionName = (String) action;
  }
  else if (action instanceof Action)  {
      actionName = (String) ((Action) action).getValue(Action.NAME);
      Action a = amap.get(actionName);
      if (a == null) amap.put(actionName, (Action) action);
  }
  else return;
  txtArea.getInputMap().put(KeyStroke.getKeyStroke(stringKey), actionName);
}
// ...

} // koniec klasy Edytor


Cel drugi:
stworzenie nowych akcji polegających na zapamiętywaniu i odtwarzaniu pozycji w tekście (tzw. bookmarks).


Tutaj musimy już skorzystać z modelu dla komponentu tekstowego - modelu realizowanego przez interfejs Document.
Model dokumentu ma jedną interesującą właściwość: umożliwia ustalenie pozycji w tekście, które będą wskazywać na ten sam fragment niezależnie od tego czy coś we wcześniejszym  tekście usuniemy lub dodamy.
Służy temu metoda createPosition, zwracająca referencję do obiektu typu Position (pozycja w tekście).

Nowe akcje (i dodatkowo przypisania klawiaturowe) stworzymy w odrębnej metodzie zarazem dodając je do naszego menu edycyjnego.

Fragment 2

Position savedPos;  // zapamiętana pozycja

void createBookmarkActionsAndKeys(JMenu eMenu) {

  final Document doc = txtArea.getDocument();

  Action setBookMark = new AbstractAction("Set bookmark") {
       public void actionPerformed(ActionEvent e) {
          int p = txtArea.getCaretPosition(); // pobiera pozycję kursora
          try {
             savedPos = doc.createPosition(p);
              } catch(Exception exc) { return; }
          }
       };

  Action goBookMark = new AbstractAction("Go to bookmark") {
       public void actionPerformed(ActionEvent e) {
          if (savedPos == null) return;
          int p = savedPos.getOffset();  // jak daleko od początku dokumentu
          txtArea.setCaretPosition(p);  // ustalenie nowej pozycji kursora
          }
        };

   eMenu.add(setBookMark);
   eMenu.add(goBookMark);
   setKey(setBookMark, "alt F12");  // wywołanie pokazanej poprzednio metody
   setKey(goBookMark, "F12");       // setKey(..)
}
 
Zadanie trzecie (chyba najciekawsze): na bieżąco uwidaczniać aktualną pozycję kursora tekstowego w kategoriach wiersz - kolumna tekstu, podając rózwnocześnie ogólną liczbę wierszy.

Rozwiazanie tego problemu wymaga bardziej aktywnego korzystania z modelu dokumentu.
Interfejs Document i implementujące go klasy zaprojektowano tak, by mogły służyć do przedstawiania najróżniejszych struktur (projekt bazuje na specyfikacji SGML).
Jednostką bazową jest element - obiekt typu Element. Elementy mogą być układane w drzewiaste hierarchie (niektóre elementy zawierają inne itd. - np. książka - rozdziały - paragrafy).
Z elementami mogą być związane zbiory atrybutów (co np. pozwala tworzyć i przetwarzać dokumenty ze stylami oraz dokumenty złożone).
W przypadku czystego tekstu mamy do czynienia z dokumentem klasy PlainDocument, który zawiera element rdzeniowy (rootElement) oraz będące jego "dziećmi" (drugi i ostatni poziom hierarchii drzewiastej) elementy - wiersze.
Aby uzyskać element rdzeniowy  dokumentu doc typu PlainDocument piszemy: doc.getDefaultRootElement(), aby uzyskać element-paragraf (w tym przypadku wiersz), który odpowiada miejscu oddalonemu o p od początku dokumentu - piszemy doc.getParagraph(p).

Obie te metody zwracają referencje do obiektu typu Element.
Interfejs Element zawiera szereg użytecznych metod, m.in. identyfikujących położenie elementów w tekście dokumentu.
Omówione wyżej zasady dają podstawę do określania numeru wiersza i kolumny w dokumencie tepu PlainText.

Aby na bieżąco śledzić zmiany pozycji kursora musimy użyć interfejsu nasłuchu kursora tekstowego - CaretListener. Wymaga on implementacji jednej metody - caretUpdate(CaretEvent), a przekazane jej zdarzenie typu CaretEvent możemy zapytać o bieżące miejsce wpisywania tekstu (getDot) oraz o miejsce do którego rozciąga się ew. zaznaczenie tekstu (getMark).
Jeśli doc jest dokumentem typu PlainDocument to określenie bieżącej pozycji kursora w kategoriach  wiersz - kolumna wygląda  tak:

public void caretUpdate(CaretEvent evt) {
 int p = evt.getDot(); // pozycja kursora
 Element e = doc.getDefaultRootElement(); // element rdzemniowy
 int nrW = e.getElementIndex(p) + 1;      // numer wiersza
 e = doc.getParagraphElement(p);          // element - wiersz
 int start = e.getStartOffset();          // na jakiej pozycji wzgledem
                                          // początku tekstu zaczyna się wiersz
 int nrKol = p-start+1;                   //numer kolumny w wierszu
}

Nie mamy tu informacji o tym ile w ogóle wierszy jest w dokumencie. Możemy ją uzyskać stosując wobec elementu rdzeniowego e odwołanie e.getElementCount().

Byłoby jednak nieroztropne pytać o to za każdym razem gdy kursor zmienia swe położenie.
Obliczanie ogólnej liczby wierszy powierzymy innemu słuchaczowi - śledzącemu zmiany w dokumencie - obiektowi typu DocumentListener.

Implemenatacja nasłuchu zmian dokumentu wymaga zdefiowania trzech metod: Pierwsze dwie są wołane wtedy, gdy do dokumentu coś dodano lub usunięto, trzecia - kiedy zmieniły się jakieś atrybuty tekstu.
 
Pozostało nam zintegrować działanie obu słuchaczy i przedstawić je w wizualnej formie. Formą tą będzie etykieta (pokazująca numer wiersza, ogólną liczbę wierszy i numer kolumny). Tworzeniem tekstu na etykiecie może zająć się obiekt klasy który jest jednocześnie eteykietą i słuchaczem kursora tekstowego. Obiekt ten - jako słuchacza - przyłączymy do komponentu tekstowego, a jako etykietę umieścimy po prawej stronie paska menu.

Słuchacza zmian dokumentu przyłączymy do dokumentu pokazywanego przez komponent tekstowy, a obliczana przy zmianach dokumentu liczba wierszy będzie przechowywana w polu docLines.

Wszystko to przedstawia fragment 3.

Fragment 3
class Edytor  extends JFrame {
...
 int docLines = 0;

 // Słuchacz zmian dokumentu

 DocumentListener docListener = new DocumentListener() {
   public void insertUpdate(DocumentEvent e) { liczbaWierszy(e); }
   public void removeUpdate(DocumentEvent e) { liczbaWierszy(e); }
   public void changedUpdate(DocumentEvent e) { }
   private void liczbaWierszy(DocumentEvent evt) {
     PlainDocument doc = (PlainDocument) evt.getDocument();
     Element e = doc.getDefaultRootElement();
     docLines = e.getElementCount();
   }
 };

 // Klasa wewnętrzna: etykieta obsługująca zmiany pozycji kursora

 class StatusLab extends JLabel implements CaretListener {

  PlainDocument doc;
  StatusLab(PlainDocument d) {
   super("Wiersz 1 / 0  Kolumna 1");
   doc = d;
  }

  public void caretUpdate(CaretEvent evt) {
     int p = evt.getDot();
     Element e = doc.getDefaultRootElement();
     int nr = e.getElementIndex(p) + 1;
     e = doc.getParagraphElement(p);
     int start = e.getStartOffset();
     setText("Wiersz "+nr+ " / "+ docLines +"  Kolumna " + (p-start+1));
  }
 }  // Koniec klasy StatusLab

 //...

 JTextArea txtArea = new JTextArea();  // to jest nasz edytor

 // Konstruktor klasy Edytor

 public Edytor() {
  PlainDocument doc = (PlainDocument) txtArea.getDocument();
  doc.addDocumentListener(docListener);
  StatusLab statusLab = new StatusLab(doc);
  txtArea.addCaretListener(statusLab);
  //...
  JMenuBar mb = new JMenuBar();
  mb.add(eMenu);  // menu edycyjne
  mb.add(Box.createHorizontalGlue());
  mb.add(statusLab);
  setJMenuBar(mb);
  // ...
 }

}

I ostatnie zadanie: wprowadzić akcje cofania edycji i przywracania wycofanych edycji (undo-redo).

Jego realizacja wymaga użycia obiektu typu UndoManager (pakiet javax.swing.undo), który przechowuje i kontroluje sekwencje wycowanych edycji oraz przyłączenia do dokumentu słuchacza cofnięć i przywróceń edycji (UndoableEditorListener), obsługującego zdarzenia typu UndoableEditEvent. Zdarzenie tego typu możemy zapytać o edycję (poprawkę), która może być wycofana (getEdit()), Edycję taką następnie przekazujemu obiektowi typu UndoManager.

Od tego obiektu możemy zarządać: wycofania edycji (undo()), przywrócenia ostatnio wycofanej edycji (redo()), oraz informacji o tym czy może wycofać edycję (canUndo()) i czy ma jakąś edycję do przywrócenia (canRedo()).
Tworzenie akcji Undo i Redo i dodanie ich do menu  umieścimy w metodzie createUndoRedo, wołanej z konstruktora naszej klasy testowej Edytor (fragment 4).
 
Fragment  4
UndoManager uman = new UndoManager();

Action undo, redo;

void createUndoRedo(JMenu eMenu) {

 undo = new AbstractAction("Undo") {
          public void actionPerformed(ActionEvent e) {
            try {
              uman.undo();
            } catch (CannotUndoException ex) { }
            setEnabled(uman.canUndo());
            redo.setEnabled(uman.canRedo());
          }
        };

 redo = new AbstractAction("Redo") {
          public void actionPerformed(ActionEvent e) {
            try {
              uman.redo();
            } catch (CannotUndoException ex) { }
            undo.setEnabled(uman.canUndo());
            setEnabled(uman.canRedo());
          }
        };

 undo.setEnabled(false);
 redo.setEnabled(false);
 eMenu.add(undo);
 eMenu.add(redo);

 Document doc = txtArea.getDocument();

 doc.addUndoableEditListener( new UndoableEditListener() {
    public void undoableEditHappened(UndoableEditEvent e) {
       uman.addEdit(e.getEdit());
       undo.setEnabled(uman.canUndo());
       redo.setEnabled(uman.canRedo());
    }
  });
}