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 |
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().
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
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]; } }
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.
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(...))
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 |
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.
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(); } }
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:
Wydruk i rysunek ilustrują omówione właściwości listy roziwjalnej.
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.
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.
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:
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). |
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:
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ą |
Bardzo łatwo określimy pięć pierwszych metod (wydruk 1):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
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
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
)
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; } }
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()
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));
Klasa JTree zapewnia hierarchiczne obrazowanie danych w postaci drzewa.
Elementy danych są wyświetlane pionow i w widoku drzewa nazywane węzłami (node) - zob. rysunek, przy czym: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(); } }
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
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(..) }
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 }
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); // ... } }
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()); } }); }