Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage
<< | < | > | >> | API | Kapitel 38 - Swing: Komponenten II

## 38.2 JTable

- 38.2 JTable
  - 38.2.1 Erzeugen von Tabellen
  - 38.2.2 Konfiguration der Tabelle
  - 38.2.3 Selektieren von Elementen
    - Selektionsmodi
    - Abfragen der Selektion
    - Verändern der Selektion
  - 38.2.4 Zugriff auf den Inhalt der Tabelle
    - Die Daten in der Tabelle
    - Editieren von Tabellenelementen
  - 38.2.5 Das Tabellenmodell
    - Beispiel
  - 38.2.6 Das Spaltenmodell
  - 38.2.7 Rendering der Zellen
  - 38.2.8 Reaktion auf Ereignisse

### 38.2.1 Erzeugen von Tabellen

Eines der in anspruchsvollen Benutzeroberflächen am häufigsten gebrauchten Dialogelemente ist die Tabelle, also eine mehrzeilige, mehrspaltige Darstellung von Daten. Diese im AWT fehlende Komponente wird in Swing durch die Klasse `JTable` zur Verfügung gestellt. Mit ihrer Hilfe lassen sich unterschiedlichste Arten von textuellen oder grafischen Daten tabellarisch darstellen und editieren. Das Programm hat dabei weitreichende Möglichkeiten, die Tabelle zu konfigurieren, ihren Inhalt anzupassen und auf Benutzerereignisse zu reagieren.

Die wichtigsten Konstruktoren von `JTable` sind:

    public JTable(Object[][] rowData, Object[] columnNames)
    public JTable(Vector rowData, Vector columnNames)
    public JTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm)
| javax.swing.JTable

An den ersten Konstruktor werden die darzustellenden Daten in Form eines zweidimensionalen Arrays übergeben. Dessen erste Dimension enthält die Zeilen, die zweite die Spalten. Zur Darstellung in der Tabelle werden die Array-Elemente mit `toString` in Strings umgewandelt. Das zweite Argument enthält ein Array mit Strings, die als Spaltenköpfe angezeigt werden.

Statt der Übergabe von Arrays kann auch der zweite Konstruktor verwendet und die Daten und Spaltenköpfe in einem `Vector` übergeben werden. In diesem Fall muß der Datenvektor `rowData` für jede Zeile einen Untervektor mit den Datenelementen dieser Zeile enthalten.

Der dritte Konstruktor stellt die allgemeinste Möglichkeit dar, eine `JTable` zu konstruieren. Hierbei werden alle drei Modelle der Tabelle explizit an den Konstruktor übergeben. Das `TableModel` stellt dabei die Daten zur Verfügung, das `TableColumnModel` definiert die Spalten, und das `ListSelectionModel` ist für die Selektion von Tabellenelementen zuständig. Werden alle drei Modelle separat instanziert und übergeben, hat das Programm die volle Kontrolle über alle Aspekte der Tabellendarstellung und -verarbeitung.

Eine einfache Tabelle läßt sich also sehr schnell erzeugen:

    001 /* Listing3804.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005 import javax.swing.*;
    006
    007 public class Listing3804
    008 extends JFrame
    009 implements TableData
    010 {
    011   public Listing3804()
    012   {
    013     super("JTable 1");
    014     addWindowListener(new WindowClosingAdapter(true));
    015     JTable table = new JTable(DATA, COLHEADS);
    016     Container cp = getContentPane();
    017     cp.add(new JLabel("Alte c\'t-Ausgaben:"), BorderLayout.NORTH);
    018     cp.add(new JScrollPane(table), BorderLayout.CENTER);
    019   }
    020
    021   public static void main(String[] args)
    022   {
    023     Listing3804 frame = new Listing3804();
    024     frame.setLocation(100, 100);
    025     frame.setSize(300, 200);
    026     frame.setVisible(true);
    027   }
    028 }
| Listing3804.java

Listing 38.4: Eine einfache Tabelle

Die Ausgabe des Programms ist:

Abbildung 38.5: Eine einfache Tabelle

|  |  |  | Wie in anderen großen Dialogelementen haben wir die `JTable` vor der Übergabe an ihren GUI-Container in eine `JScrollPane` verpackt. Neben dem Effekt, die Daten vertikal oder horizontal scrollen zu können, hat das vor allem zur Folge, daß die Spaltenköpfe angezeigt werden. Ohne `JScrollPane` wären sie dagegen nicht sichtbar. |  |
Warnung

In Listing 38.4 wurden zwei Konstanten `DATA` und `COLHEADS` verwendet. Sie dienen als Beispieldaten für die Programme dieses Abschnitts und wurden als Konstanten in dem Interface `TableData` definiert:

    001 /* TableData.java */
    002
    003 public interface TableData
    004 {
    005   public static final String[][] DATA = {
    006     {" 1/1987", "195", "Vergleichstest EGA-Karten"},
    007     {" 2/1987", "171", "Schneider PC: Bewährungsprobe"},
    008     {" 3/1987", "235", "Luxus-Textsyteme im Vergleich"},
    009     {" 4/1987", "195", "Turbo BASIC"},
    010     {" 5/1987", "211", "640-K-Grenze durchbrochen"},
    011     {" 6/1987", "211", "Expertensysteme"},
    012     {" 7/1987", "199", "IBM Model 30 im Detail"},
    013     {" 8/1987", "211", "PAK-68: Tuning für 68000er"},
    014     {" 9/1987", "215", "Desktop Publishing"},
    015     {"10/1987", "279", "2,5 MByte im ST"},
    016     {"11/1987", "279", "Transputer-Praxis"},
    017     {"12/1987", "271", "Preiswert mit 24 Nadeln"},
    018     {" 1/1988", "247", "Schnelle 386er"},
    019     {" 2/1988", "231", "Hayes-kompatible Modems"},
    020     {" 3/1988", "295", "TOS/GEM auf 68020"},
    021     {" 4/1988", "263", "Projekt Super-EGA"},
    022     {" 5/1988", "263", "Neuheiten auf der CeBIT 88"},
    023     {" 6/1988", "231", "9600-Baud-Modem am Postnetz"}
    024   };
    025
    026   public static final String[] COLHEADS = {
    027     "Ausgabe", "Seiten", "Titelthema"
    028   };
    029 }
| TableData.java

Listing 38.5: Das Interface TableData

Auf diese Weise können die Daten in den folgenden Beispielen dieses Abschnitts einfach mit der Anweisung `implements TableData` importiert und dem Programm zur Verfügung gestellt werden. Diese - auf den ersten Blick - etwas ungewöhnliche Verwendung eines Interfaces ist ein Standard-Idiom in Java und wurde in Abschnitt 9.4.1 erläutert.

### 38.2.2 Konfiguration der Tabelle

Eine `JTable` läßt sich auf vielfältige Weise konfigurieren. Mit `setRowHeight` wird die Gesamthöhe einer Zeile festgelegt, alle Zeilen sind dabei gleich hoch. Mit `setRowMargin` wird der am oberen und unteren Rand jeder Zelle freibleibende Platz bestimmt. Der für den Inhalt der Zelle verfügbare Platz ergibt sich aus der Zellenhöhe minus oberem und unterem Rand. Durch Aufruf von `setIntercellSpacing` kann (zusammen mit dem vertikalen) auch der horizontale Rand der Zellenelemente festgelegt werden:

    public void setRowHeight(int newHeight)
    public void setRowMargin(int rowMargin)
    public void setIntercellSpacing(Dimension newSpacing)
| javax.swing.JTable

Standardmäßig werden die Zellen einer `JTable` mit senkrechten und waagerechten Begrenzungslinien voneinander getrennt. Mit `setShowGrid` können beide Linienarten zugleich an- oder ausgeschaltet werden. Sollen die horizontalen oder vertikalen Linien separat aktiviert oder deaktiviert werden, können die Methoden `setShowHorizontalLines` und `setShowVerticalLines` verwendet werden:

    public void setShowGrid(boolean b)
    public void setShowHorizontalLines(boolean b)
    public void setShowVerticalLines(boolean b)
| javax.swing.JTable

Das Verändern der Farben der Zellen ist in begrenzter Weise mit folgenden Methoden möglich:

    public void setGridColor(Color newColor)
    public void setSelectionForeground(Color selectionForeground)
    public void setSelectionBackground(Color selectionBackground)
| javax.swing.JTable

`setGridColor` verändert die Farbe, in der die Gitterlinien angezeigt werden. Mit `setSelectionForeground` und `setSelectionBackground` wird die Vorder- und Hintergrundfarbe des selektierten Bereichs festgelegt.

Als letzte der Konfigurationsmethoden wollen wir uns `setAutoResizeMode` ansehen:

    public void setAutoResizeMode(int mode)
| javax.swing.JTable

Sie bestimmt das Verhalten der Tabelle, nachdem die Breite einer einzelnen Spalte verändert wurde. Der dadurch freiwerdende oder zusätzlich benötigte Platz kann nämlich auf unterschiedliche Weise den übrigen Spalten zugeordnet werden. Der Parameter `mode` kann folgende Werte annehmen:

Modus | Bedeutung
`AUTO_RESIZE_OFF` | Es erfolgt keine automatische Größenanpassung der übrigen Spalten. Wurde die Tabelle in `JScrollPane` verpackt, bekommt sie nötigenfalls einen horizontalen Schieberegler.
`AUTO_RESIZE_LAST_COLUMN` | Die letzte Spalte wird zum Größenausgleich verwendet. Dadurch reduziert sich der Platz für die letzte Spalte, wenn eine andere Spalte vergrößert wird, und er erhöht sich, wenn sie verkleinert wird.
`AUTO_RESIZE_NEXT_COLUMN` | Die rechts neben der modifizierten Spalte liegende Spalte wird zum Größenausgleich verwendet.
`AUTO_RESIZE_SUBSEQUENT_COLUMNS` | Die Größenänderung wird gleichmäßig auf alle nachfolgenden Spalten verteilt.
`AUTO_RESIZE_ALL_COLUMNS` | Die Größenänderung wird auf alle Spalten der Tabelle verteilt.

Tabelle 38.2: Parameter für setAutoResizeMode

### 38.2.3 Selektieren von Elementen

#### Selektionsmodi

Die Elemente einer `JTable` können auf unterschiedliche Weise selektiert werden. Welche Möglichkeiten der Selektion dem Anwender zur Verfügung gestellt werden, regeln die folgenden Methoden:

    public void setRowSelectionAllowed(boolean flag)
    public void setColumnSelectionAllowed(boolean flag)
    public void setSelectionMode(int selectionMode)
    public void setCellSelectionEnabled(boolean flag)
| javax.swing.JTable

Soll zeilenweise selektiert werden, ist `setRowSelectionAllowed` mit `true` als Argument aufzurufen. Soll spaltenweise selektiert werden, ist analog `setColumnSelectionAllowed` aufzurufen. Durch Übergabe von `false` können beide Selektionsarten ausgeschaltet werden und nur noch einzelne Zellen selektiert werden. Standardmäßig kann zeilen-, aber nicht spaltenweise selektiert werden.

Mit `setSelectionMode` wird festgelegt, ob ein einzelnes Element, ein zusammenhängender Bereich oder mehrere Bereiche selektiert werden können. Hier ist eine der in Abschnitt 37.3.1 beschriebenen Konstanten `SINGLE_SELECTION`, `SINGLE_INTERVAL_SELECTION` oder `MULTIPLE_INTERVAL_SELECTION` der Klasse `ListSelectionModel` zu übergeben. Wird `setCellSelection` mit `true` als Argument aufgerufen, können Zeilen und Spalten gleichzeitig markiert und so zusammenhängende rechteckige Bereiche von Zellen (einschließlich einer einzelnen) selektiert werden.

#### Abfragen der Selektion

Um herauszufinden, welche Elemente selektiert wurden, können folgende Methoden verwendet werden:

    public int getSelectedRow()
    public int getSelectedColumn()

    public int[] getSelectedRows()
    public int[] getSelectedColumns()
| javax.swing.JTable

`getSelectedRow` und `getSelectedColumn` liefern die selektierte Zeile bzw. Spalte, wenn der Selektionsmodus `SINGLE_SELECTION` ist. Die erste Zeile und Spalte haben dabei jeweils den Index 0. Erlaubt der aktuelle Selektionsmodus das Selektieren ganzer Zeilen oder Spalten, impliziert das Ergebnis, daß alle Elemente dieser Zeile bzw. Spalte selektiert sind. Ist einer der Mehrfachselektionsmodi aktiviert, können mit `getSelectedRows` und `getSelectedColumns` Arrays mit allen selektierten Zeilen und Spalten beschafft werden.

|  | Falls keine Elemente selektiert sind, geben `getSelectedRow` und `getSelectedColumn` -1 und `getSelectedRows` und `getSelectedColumns` ein leeres Array zurück. |  |
Hinweis

#### Verändern der Selektion

`JTable` stellt auch Methoden zur Verfügung, mit denen die Selektion programmgesteuert verändert werden kann:

    public void selectAll()
    public void clearSelection()

    public void setRowSelectionInterval(int index0, int index1)
    public void addRowSelectionInterval(int index0, int index1)
    public void removeRowSelectionInterval(int index0, int index1)

    public void setColumnSelectionInterval(int index0, int index1)
    public void addColumnSelectionInterval(int index0, int index1)
    public void removeColumnSelectionInterval(int index0, int index1)
| javax.swing.JTable

Mit `selectAll` kann die komplette Tabelle markiert werden, mit `clearSelection` wird die Selektion entfernt. Mit `setRowSelectionInterval` kann ein zusammenhängender Bereich von Zeilen markiert werden. Mit `addRowSelectionInterval` wird ein solcher zur aktuellen Selektion hinzugefügt und mit `removeRowSelectionInterval` daraus entfernt. Für die Selektion von Spalten stehen die analogen Methoden `setColumnSelectionInterval`, `addColumnSelectionInterval` und `removeColumnSelectionInterval` zur Verfügung.

|  |  |  | Damit die beschriebenen Methoden korrekt funktionieren, sollte ihr Aufruf in Einklang mit den aktuell gewählten Selektionsmodi stehen. Im RC1 des JDK 1.3 gab es beispielsweise Probleme, wenn `selectAll` auf einer Tabelle aufgerufen wurde, die nur Einfachselektion erlaubte. Nach dem Aufruf wurde zwar (korrekterweise) keine Selektion mehr angezeigt, die Methoden zur Abfrage der selektierten Elemente verhielten sich aber dennoch so, als wären alle Elemente selektiert. Bei Verwendung von `addRowSelectionInterval` fiel dagegen auf, daß die Methode nur dann korrekt funktionierte, wenn der Selektionsmodus `MULTIPLE_INTERVAL_SELECTION` aktiviert war. |  |
Warnung

### 38.2.4 Zugriff auf den Inhalt der Tabelle

#### Die Daten in der Tabelle

Unabhängig von der aktuellen Selektion kann natürlich auch auf den Inhalt der Tabelle zugegriffen werden:

    public int getRowCount()
    public int getColumnCount()

    public Object getValueAt(int row, int column)
    public void setValueAt(Object aValue, int row, int column)
| javax.swing.JTable

`getRowCount` und `getColumnCount` liefern die aktuelle Zeilen- bzw. Spaltenzahl der Tabelle. Mit `getValueAt` kann auf das Element an der Position (row, column) zugegriffen werden. Beide Indices beginnen bei 0, ein Zugriff außerhalb der Grenzen wird mit einer `ArrayIndexOutOfBoundsException` quittiert. Mit `setValueAt` kann ein Zellenelement sogar verändert werden.

|  |  |  | Bei der Verwendung der Methoden `getValueAt` und `setValueAt` ist es wichtig zu wissen, daß die angegebenen Zeilen- und Spaltenwerte sich auf die aktuelle Ansicht der Tabelle beziehen, nicht auf ihr Modell. Hat der Anwender beispielsweise die Spalten eins und zwei vertauscht, würde ein Zugriff auf ein Element in Spalte eins den Modellwert in Spalte zwei verändern und umgekehrt. Während dieses Verhalten erwartungskonform ist, wenn der Wert durch den Anwender editiert wird, würde es bei programmgesteuertem Aufruf zu einem logischen Fehler kommen, denn das Programm hat natürlich zunächst einmal keine Kenntnis davon, daß der Anwender die Spaltenreihenfolge verändert hat. In aller Regel werden programmgesteuerte Zugriffe auf einzelne Zellen daher nicht mit `getValueAt` und `setValueAt` ausgeführt, sondern an die gleichnamigen Methoden des `TableModel` delegiert. |  |
Warnung

#### Editieren von Tabellenelementen

Nach einem Doppelklick auf eine Zelle kann der Anwender die in diesem Element enthaltenen Daten verändern. `JTable` besitzt einige Methoden, mit denen das Programm abfragen kann, ob und in welcher Zelle die Tabelle gerade editiert wird:

    public boolean isEditing()
    public int getEditingRow()
    public int getEditingColumn()
| javax.swing.JTable

`isEditing` gibt genau dann `true` zurück, wenn gerade ein Element der Tabelle geändert wird. Mit `getEditingRow` und `getEditingColumn` kann das Programm herausfinden, welches Element betroffen ist. Wird keine Zelle editiert, geben die Methoden -1 zurück. Zudem kann das Programm durch Aufruf von `editCellAt` selbst das Editieren eines Tabellenelements einleiten:

    public boolean editCellAt(int row, int column)
| javax.swing.JTable

|  | Unabhängig von seinem bisherigen Typ wird der geänderte Wert nach Abschluß des Änderungsvorgangs als `String` in das Modell zurückgeschrieben. Wird - wie im letzten Beispiel - ein `Object`-Array als Modell verwendet, ist diese Typkonvertierung zwar korrekt, kann aber bei der Weiterverarbeitung des Modells zu Überraschungen führen. Besser ist es, Eingaben des Anwenders direkt nach der Eingabe zu prüfen und vor der Speicherung in den passenden Typ umzuwandeln. Wir werden im nächsten Abschnitt zeigen, wie man das mit Hilfe eines eigenen Tabellenmodells erreichen kann. |  |
Hinweis

### 38.2.5 Das Tabellenmodell

Für einfache Anwendungen reicht es aus, mit den automatisch erzeugten Tabellenmodellen zu arbeiten. Für Anwendungen mit komplexer strukturierten Daten, oder solchen, die für ein Array zu umfangreich oder an externe Quellen gebundenen sind, ist es dagegen sinnvoll, ein eigenes Tabellenmodell zu implementieren. Dieses muß das Interface `TableModel` aus dem Paket `javax.swing.table` implementieren und bei der Instanzierung an den Konstruktor der `JTable` übergeben. Wahlweise kann auch nach der Instanzierung auf das Modell zugegriffen werden:

    public void setModel(TableModel newModel)
    public TableModel getModel()
| javax.swing.JTable

Das Interface `TableModel` definiert folgende Methoden:

    public int getRowCount()
    public int getColumnCount()

    public String getColumnName(int columnIndex)
    public Class getColumnClass(int columnIndex)

    public boolean isCellEditable(int rowIndex, int columnIndex)

    public Object getValueAt(int rowIndex, int columnIndex)
    public void setValueAt(Object aValue, int rowIndex, int columnIndex)

    public void addTableModelListener(TableModelListener l)
    public void removeTableModelListener(TableModelListener l)
| javax.swing.table.TableModel

Die meisten von ihnen sind Service-Methoden. Sie werden von `JTable` aufgerufen, um Informationen zur Darstellung der Tabelle zu erhalten. `getRowCount` und `getColumnCount` liefern die Anzahl der Zeilen und Spalten, `getColumnName` die Spaltenüberschrift und `getColumnClass` den Typ der Elemente einer Spalte. Mit `isCellEditable` wird abgefragt, ob eine bestimmte Zelle editiert werden darf oder nicht. Mit `getValueAt` fragt die Tabelle beim Modell nach dem Wert einer bestimmten Zelle, und mit `setValueAt` wird ein geänderter Wert in das Modell zurückgeschrieben.

Mit den Methoden `addTableModelListener` und `removeTableModelListener` kann ein `TableModelListener` registriert bzw. deregistriert werden. Er wird über alle Änderungen des Modells unterrichtet und damit insbesondere aufgerufen, wenn eine Zeile oder Spalte eingefügt oder gelöscht wurde, wenn der Inhalt einer Zelle modifiziert wurde oder wenn die Gesamtstruktur des Modells sich geändert hat. Typischerweise registriert sich die `JTable` bei ihrem Modell, um auf Modelländerungen mit entsprechenden Änderungen der Benutzeroberfläche reagieren zu können.

#### Beispiel

Als Beispiel wollen wir ein Modell konstruieren, das eine sehr große Tabelle repräsentieren kann (z.B. mit 1000 mal 1000 Elementen), von denen aber nur sehr wenige tatsächlich einen Wert enthalten und alle anderen leer sind. Statt einer speicherintensiven Darstellung mittels eines entsprechend dimensionierten Arrays sollen nur die tatsächlich belegten Elemente gespeichert werden. Wir wollen dazu eine `Hashtable` verwenden, deren Elemente die tatsächlich vorhandenen Werte sind. Als Schlüssel verwenden wir eine String-Darstellung der Koordinaten des Elements. Der Zugriff auf ein Element erfolgt dann, indem dessen Koordinatenschlüssel in der `Hashtable` gesucht und der zugehörige Wert zurückgegeben bzw. gespeichert wird.

Um das Tabellenmodell nicht von Grund auf neu entwickeln zu müssen, leiten wir es aus der Klasse `AbstractTableModel` des Pakets `javax.swing.table` ab. Diese bietet für fast alle erforderlichen Methoden Standardimplementierungen und stellt darüber hinaus einige nützliche Hilfsmethoden zur Verfügung:

    001 /* SparseTableModel.java */
    002
    003 import java.util.*;
    004 import javax.swing.*;
    005 import javax.swing.table.*;
    006
    007 public class SparseTableModel
    008 extends AbstractTableModel
    009 {
    010   private int size;
    011   private Hashtable data;
    012
    013   //Konstruktor
    014   public SparseTableModel(int size)
    015   {
    016     this.size = size;
    017     this.data = new Hashtable();
    018   }
    019
    020   //Methoden für das TableModel-Interface
    021   public int getRowCount()
    022   {
    023     return size;
    024   }
    025
    026   public int getColumnCount()
    027   {
    028     return size;
    029   }
    030
    031   public String getColumnName(int columnIndex)
    032   {
    033     return "C" + columnIndex;
    034   }
    035
    036   public Class getColumnClass(int columnIndex)
    037   {
    038     return String.class;
    039   }
    040
    041   public boolean isCellEditable(int rowIndex, int columnIndex)
    042   {
    043     return rowIndex < size && columnIndex < size;
    044   }
    045
    046   public Object getValueAt(int rowIndex, int columnIndex)
    047   {
    048     String key = "[" + rowIndex + "," + columnIndex + "]";
    049     String value = (String)data.get(key);
    050     return value == null ? "-" : value;
    051   }
    052
    053   public void setValueAt(Object aValue, int rowIndex, int columnIndex)
    054   {
    055     String key = "[" + rowIndex + "," + columnIndex + "]";
    056     String value = (String)aValue;
    057     if (value.length() <= 0) {
    058       data.remove(key);
    059     } else {
    060       data.put(key, value);
    061     }
    062   }
    063
    064   //Zusätzliche Methoden
    065   public void printData()
    066   {
    067     Enumeration e = data.keys();
    068     while (e.hasMoreElements()) {
    069       String key = (String)e.nextElement();
    070       System.out.println(
    071         "At " + key + ": " + (String)data.get(key)
    072       );
    073     }
    074   }
    075 }
| SparseTableModel.java

Listing 38.6: Ein Modell für schwach besetzte Tabellen

Die Klasse wird durch Übergabe der Anzahl der Zeilen und Spalten instanziert. `getRowCount` und `getColumnCount` liefern genau diesen Wert zurück. Als Spaltenname wird ein "C", gefolgt von der Nummer der Spalte angegeben. Alle Spalten sind vom Typ `String` und alle Zellen sind editierbar. Wird mit `getValueAt` der Inhalt einer bestimmten Tabellenzelle abgefragt, so bildet die Methode den Schlüssel aus Zeilen- und Spaltenindex und sucht damit in der `Hashtable` `data`. Falls ein Eintrag gefunden wird, gibt `getValueAt` diesen an den Aufrufer zurück, andernfalls wird nur ein Minuszeichen geliefert. `setValueAt` arbeitet analog. Auch hier wird zunächst der Schlüssel gebildet und dann zusammen mit dem zugehörigen Wert in der `Hashtable` gespeichert. Die Hilfemethode `printData` dient dazu, alle vorhandenen Werte samt Koordinatenschlüsseln auf der Konsole auszugeben.

Mit Hilfe dieses Modells Tabellen zu bauen, die auch bei großen Abmessungen noch effizient arbeiten, ist nicht mehr schwer. Das folgende Programm zeigt das am Beispiel einer Tabelle mit einer Million Zellen. Neben der Tabelle enthält es einen Button "Drucken", mit dem die aktuelle Belegung der Tabelle ausgegeben werden kann.

    001 /* Listing3807.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005 import javax.swing.*;
    006
    007 public class Listing3807
    008 extends JFrame
    009 implements ActionListener
    010 {
    011   JTable table;
    012   SparseTableModel tableModel;
    013
    014   public Listing3807()
    015   {
    016     super("JTable 2");
    017     addWindowListener(new WindowClosingAdapter(true));
    018     tableModel = new SparseTableModel(1000);
    019     table = new JTable(tableModel, null);
    020     table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    021     table.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
    022     table.setCellSelectionEnabled(true);
    023     Container cp = getContentPane();
    024     cp.add(new JScrollPane(table), BorderLayout.CENTER);
    025     JButton button = new JButton("Drucken");
    026     button.addActionListener(this);
    027     cp.add(button, BorderLayout.SOUTH);
    028   }
    029
    030   public void actionPerformed(ActionEvent event)
    031   {
    032     tableModel.printData();
    033   }
    034
    035   public static void main(String[] args)
    036   {
    037     Listing3807 frame = new Listing3807();
    038     frame.setLocation(100, 100);
    039     frame.setSize(320, 200);
    040     frame.setVisible(true);
    041   }
    042 }
| Listing3807.java

Listing 38.7: Eine JTable mit einer Million Zellen

Die Ausgabe des Programms sieht nach einigen Einfügungen so aus:

Abbildung 38.6: Eine JTable mit einer Million Zellen

Wenn in diesem Zustand der "Drucken"-Button betätigt wird, gibt das Programm folgende Liste auf der Konsole aus:

    At [997,998]: große
    At [994,997]: Hallo
    At [999,999]: Welt
    At [996,999]: Tabellen-

### 38.2.6 Das Spaltenmodell

Neben dem Tabellenmodell, das die Daten der Tabelle enthält, besitzt eine `JTable` ein weiteres Modell, das für die Eigenschaften der Spalten verantwortlich ist. In unseren bisherigen Beispielen wurde es implizit aus dem Tabellenmodell und den angegebenen Spaltennamen erzeugt. Sollen neben den Namen weitere Eigenschaften der Spalten kontrolliert werden, reicht das nicht aus, und ein eigenes Spaltenmodell muß geschrieben werden.

Das Spaltenmodell einer `JTable` muß das Interface `TableColumnModel` aus dem Paket `javax.swing.table` implementieren und wird bei der Instanzierung einer `JTable` an deren Konstruktor übergeben. Da die Implementierung eines Spaltenmodells recht aufwendig ist, wurde mit der Klasse `DefaultTableColumnModel` eine Standard-Implementierung geschaffen, die ohne weitere Ableitung verwendet werden kann. Das zunächst leere Modell stellt Methoden zur Verfügung, mit denen Spaltenobjekte (sie sind vom Typ `TableColumn`) hinzugefügt oder entfernt werden können:

    public void addColumn(TableColumn aColumn)
    public void removeColumn(TableColumn column)
| javax.swing.table.DefaultTableColumnModel

Jede an das Modell übergebene Instanz der Klasse `TableColumn` repräsentiert dabei die Eigenschaften einer einzelnen Tabellenspalte. Mit einer `TableColumn` können praktisch alle visuellen Eigenschaften der Spalte kontrolliert werden. So kann die Breite ebenso wie die Spaltenposition festgelegt werden, und es können beliebige Komponenten zur Darstellung und zum Editieren der Zellen definiert werden. Wie wollen uns auf ein einfaches Beispiel beschränken und lediglich zeigen, wie die anfängliche Breite der Spalten explizit festgelegt werden kann.

Dazu instanzieren wir ein `DefaultTableColumnModel` und fügen drei `TableColumn`-Objekte hinzu. Sie werden jeweils mit folgendem Konstruktor initialisiert:

    public TableColumn(int modelIndex, int width)
| javax.swing.table.TableColumn

Der erste Parameter gibt den Modellindex an, also die Spalte im Tabellenmodell, zu der die visuelle Spalte korrespondiert. Der zweite Parameter gibt die initiale Breite der Spalte an. Anschließend rufen wir die Methode `setHeaderValue` auf, um die Spaltenbeschriftung zu definieren, und fügen die Spalte in das Spaltenmodell ein. Das wiederholen wir für alle drei Spalten und übergeben das Spaltenmodell an den Konstruktor der Tabelle. Da bei Übergabe eines Spaltenmodells auch das Tabellenmodell explizit übergeben werden muß, definieren wir es aus unserem vorhandenen Datenarray durch eine lokale Ableitung der Klasse `AbstractTableModel`:

    001 /* Listing3808.java */
    002
    003 import java.awt.*;
    004 import javax.swing.*;
    005 import javax.swing.table.*;
    006
    007 public class Listing3808
    008 extends JFrame
    009 implements TableData
    010 {
    011   public Listing3808()
    012   {
    013     super("JTable 3");
    014     addWindowListener(new WindowClosingAdapter(true));
    015     //Spaltenmodell erzeugen
    016     DefaultTableColumnModel cm = new DefaultTableColumnModel();
    017     for (int i = 0; i < COLHEADS.length; ++i) {
    018       TableColumn col = new TableColumn(i, i == 2 ? 150 : 60);
    019       col.setHeaderValue(COLHEADS[i]);
    020       cm.addColumn(col);
    021     }
    022     //Tabellenmodell erzeugen
    023     TableModel tm = new AbstractTableModel() {
    024       public int getRowCount()
    025       {
    026         return DATA.length;
    027       }
    028       public int getColumnCount()
    029       {
    030         return DATA[0].length;
    031       }
    032       public Object getValueAt(int row, int column)
    033       {
    034         return DATA[row][column];
    035       }
    036     };
    037     //Tabelle erzeugen und ContentPane füllen
    038     JTable table = new JTable(tm, cm);
    039     Container cp = getContentPane();
    040     cp.add(new JLabel("Alte c\'t-Ausgaben:"), BorderLayout.NORTH);
    041     cp.add(new JScrollPane(table), BorderLayout.CENTER);
    042   }
    043
    044   public static void main(String[] args)
    045   {
    046     Listing3808 frame = new Listing3808();
    047     frame.setLocation(100, 100);
    048     frame.setSize(350, 200);
    049     frame.setVisible(true);
    050   }
    051 }
| Listing3808.java

Listing 38.8: Eine JTable mit einem eigenen Spaltenmodell

Die initialen Spaltenbreiten wurden auf 60 bzw. 150 Zeichen festgelegt, und die Ausgabe des Programms sieht so aus:

Abbildung 38.7: Eine JTable mit eigenem Spaltenmodell

### 38.2.7 Rendering der Zellen

Als Rendering bezeichet man den Vorgang, der dafür sorgt, daß die Zellen auf dem Bildschirm dargestellt werden. Die dafür verantwortlichen Komponenten werden als Renderer bezeichnet. Eine `JTable` besitzt einen Standard-Renderer, auf den mit den Methoden `getDefaultRenderer` und `setDefaultRenderer` zugegriffen werden kann:

    public TableCellRenderer getDefaultRenderer(Class columnClass)
    public void setDefaultRenderer(Class columnClass, TableCellRenderer renderer)
| javax.swing.JTable

Sofern nicht in den Tabellenspalten ein eigener Renderer bestimmt wird, ist der Standard-Renderer für die Darstellung aller Tabellenelemente zuständig. Er muß das Interface `TableCellRenderer` implementieren. Es enthält nur eine einzige Methode:

    public Component getTableCellRendererComponent(
      JTable table,
      Object value,
      boolean isSelected,
      boolean hasFocus,
      int row,
      int column
    )
| javax.swing.table.TableCellRenderer

Diese arbeitet als Factory-Methode und wird immer dann aufgerufen, wenn zur Darstellung einer Zelle ein Renderer benötigt wird. Mit Hilfe der übergebenen Argumente kann der Renderer bestimmen, für welche Zelle er aktiv werden soll, welchen Inhalt diese hat, und ob sie gerade selektiert ist oder den Fokus hat. Zusätzlich wird die Tabelle selbst übergeben, so daß der Renderer Zugriff auf deren Eigenschaften und Modelle hat.

Standardmäßig wird als Renderer eine Instanz der Klasse `DefaultTableCellRenderer` verwendet. Sie ist eine Ableitung von `JLabel`, mit deren Hilfe Farbe, Font und Hintergrund an das Look-and-Feel der Tabelle und die Erfordernisse der jeweiligen Zelle anpaßt werden. Interessanterweise wird pro Tabelle lediglich eine einzige Instanz erzeugt und zur Darstellung aller Zellen verwendet. Dazu wird das Label jeweils an die Position der darzustellenden Tabelle verschoben und dann mit den erforderlichen visuellen Eigenschaften versehen.

Da ein `JLabel` für diese Art von Anwendung eigentlich nicht vorgesehen wurde, muß `DefaultTableCellRenderer` aus Performancegründen (insbesondere im JDK 1.3) einige der Standardmechanismen von Swing-Komponenten deaktivieren oder umdefinieren. Aus diesem Grunde ist das Ableiten einer eigenen Klasse aus `DefaultTableCellRenderer` problematisch. Auch das Verwenden des Standard-Renderers in einer eigenen Renderer-Implementierung (mit dem Ziel, nur die wirklich unterschiedlichen Eigenschaften zu modifizieren) funktioniert nicht ohne weiteres.

Das folgende Beispiel zeigt einen Renderer, dessen Aufgabe darin besteht, die Zellen unserer schon bekannten Tabelle in unterschiedlichen Farben darzustellen. Die Klasse `DefaultTableCellRenderer` wird dazu weder per Ableitung noch per Delegation verwendet.

    001 /* ColoredTableCellRenderer.java */
    002
    003 import java.awt.*;
    004 import javax.swing.*;
    005 import javax.swing.border.*;
    006 import javax.swing.table.*;
    007
    008 public class ColoredTableCellRenderer
    009 implements TableCellRenderer
    010 {
    011   private Color lightBlue = new Color(160, 160, 255);
    012   private Color darkBlue  = new Color( 64,  64, 128);
    013
    014   public Component getTableCellRendererComponent(
    015     JTable table,
    016     Object value,
    017     boolean isSelected,
    018     boolean hasFocus,
    019     int row,
    020     int column
    021   )
    022   {
    023     //Label erzeugen
    024     JLabel label = new JLabel((String)value);
    025     label.setOpaque(true);
    026     Border b = BorderFactory.createEmptyBorder(1, 1, 1, 1);
    027     label.setBorder(b);
    028     label.setFont(table.getFont());
    029     label.setForeground(table.getForeground());
    030     label.setBackground(table.getBackground());
    031     if (hasFocus) {
    032       label.setBackground(darkBlue);
    033       label.setForeground(Color.white);
    034     } else if (isSelected) {
    035       label.setBackground(lightBlue);
    036     } else {
    037       //Angezeigte Spalte in Modellspalte umwandeln
    038       column = table.convertColumnIndexToModel(column);
    039       if (column == 1) {
    040         int numpages = Integer.parseInt((String)value);
    041         if (numpages >= 250) {
    042           label.setBackground(Color.red);
    043         } else if (numpages >= 200) {
    044           label.setBackground(Color.orange);
    045         } else {
    046           label.setBackground(Color.yellow);
    047         }
    048       }
    049     }
    050     return label;
    051   }
    052 }
| ColoredTableCellRenderer.java

Listing 38.9: Ein eigener Zellrenderer

`getTableCellRendererComponent` erzeugt bei jedem Aufruf ein neues `JLabel`, dessen Beschriftung dem Zelleninhalt entspricht. Es bekommt einen nicht-transparenten Hintergrund und einen unsichtbaren Rahmen von einem Pixel Breite (damit die Zellen nicht direkt aneinanderstoßen). Anschließend werden Schriftart, Vorder- und Hintergrundfarbe von der Tabelle übernommen.

Ab Zeile 031 beginnt die Definition der Vorder- und Hintergrundfarbe. Hat das Element den Fokus, wird es in dunkelblau auf weiß gezeichnet. Ist es lediglich selektiert, wird der Hintergrund hellblau eingefärbt. Ist beides nicht der Fall, prüft die Methode, ob das darzustellende Element aus der Spalte mit den Seitenzahlen stammt. Dazu ist es zunächst nötig, in Zeile 038 den visuellen Spaltenwert in die korrespondierende Modellspalte umzurechnen (vertauscht der Anwender Spalten, unterscheiden sich beide Werte). Abhängig von der vorgefundenen Seitenzahl wird der Hintergrund dann gelb, orange oder rot dargestellt.

Dieser Renderer kann sehr leicht durch Aufruf von `setDefaultRenderer` in die Tabelle integriert werden:

    001 /* Listing3810.java */
    002
    003 import java.awt.*;
    004 import javax.swing.*;
    005
    006 public class Listing3810
    007 extends JFrame
    008 implements TableData
    009 {
    010   public Listing3810()
    011   {
    012     super("JTable 4");
    013     addWindowListener(new WindowClosingAdapter(true));
    014     JTable table = new JTable(DATA, COLHEADS);
    015     table.setDefaultRenderer(
    016       Object.class,
    017       new ColoredTableCellRenderer()
    018     );
    019     Container cp = getContentPane();
    020     cp.add(new JLabel("Alte c\'t-Ausgaben:"), BorderLayout.NORTH);
    021     cp.add(new JScrollPane(table), BorderLayout.CENTER);
    022   }
    023
    024   public static void main(String[] args)
    025   {
    026     Listing3810 frame = new Listing3810();
    027     frame.setLocation(100, 100);
    028     frame.setSize(350, 200);
    029     frame.setVisible(true);
    030   }
    031 }
| Listing3810.java

Listing 38.10: Eine Tabelle mit einem eigenen Zellrenderer

Die Ausgabe des Programmes sieht nun so aus:

Abbildung 38.8: Eine Tabelle mit einem eigenen Zellrenderer

|  |  |  | Der Renderer erzeugt bei jedem Aufruf von `getTableCellRendererComponent` eine neue Instanz der Klasse `JLabel`. Da das während der Arbeit mit der Tabelle sehr häufig erfolgt (schon das Bewegen des Mauszeigers über der Tabelle löst etliche Aufrufe aus), ist diese Vorgehensweise recht ineffizient und belastet den Garbage Collector. In "echten" Programmen sollte daher mehr Aufwand getrieben werden. So könnten beispielsweise Renderer in einem Cache zwischengespeichert und bei erneutem Bedarf wiederverwendet werden. Oder das Programm könnte eine Technik ähnlich der von `DefaultTableCellRenderer` verwenden und nur eine einzige Instanz erzeugen. Die Lektüre des Quelltextes der Klasse zeigt, wie es gemacht wird. |  |
Warnung

### 38.2.8 Reaktion auf Ereignisse

Eine `JTable` generiert eine Vielzahl von Ereignissen, um registrierte Listener über Änderungen des Tabellenzustands zu informieren. Will ein Objekt beispielsweise darüber informiert werden, daß sich die Selektion geändert hat, muß es zwei `ListSelectionListener` registrieren. Einer davon wird auf dem Selektionsmodell registriert, das mit `getSelectionModel` ermittelt werden kann. Da dieser nur Informationen über Änderungen an der Zeilenselektion versendet, muß ein zweiter Listener auf dem Modell für die Spaltenselektion registriert werden. Es kann durch Aufruf von `getColumnModel` beschafft werden, und auf sein Selektionsmodell kann ebenfalls mit `getSelectionModel` zugegriffen werden. Bei jeder Änderung der Selektion wird nun `valueChanged` aufgerufen und kann mit Hilfe der oben erläuterten Methoden herausfinden, welche Zeilen und Spalten selektiert sind.

Die Tabelle informiert auch über Änderungen ihrer Daten. Dazu muß auf dem Tabellenmodell (das mit `getModel` beschafft wird) durch Aufruf von `addTableModelListener` ein `TableModelListener` registriert werden. Bei jeder Änderung des Modells wird dann dessen Methode `tableChanged` aufgerufen.

Schließlich können auch alle in den Vaterklassen von `JTable` definierten Listener registriert werden. Soll beispielsweise auf einen Klick mit der rechten Maustaste reagiert werden, kann durch Aufruf von `addMouseListener` ein `MouseListener` registriert werden. Innerhalb seiner Ereignismethoden kann mit `getX` und `getY` die aktuelle Mausposition abgefragt und mit den Methoden `rowAtPoint` und `columnAtPoint` in Zeilen- und Spaltenwerte der Tabelle umgerechnet werden:

    public int rowAtPoint(Point point)
    public int columnAtPoint(Point point)
| javax.swing.JTable

Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0
<< | < | > | >> | API | © 1998-2002 Guido Krüger, http://www.javabuch.de