15.26 Tabellen mit JTable
Mit der Klasse JTable lassen sich auf einfache Weise zweidimensionale Tabellendaten darstellen. Die Java-Bibliothek enthält dafür eine einfache Schnittstelle, die über ein Modell und eine eigene Visualisierung ergänzt werden kann. Die vorgefertigte Implementierung bietet schon vieles an, wie zum Beispiel Änderung der Spaltenbreite, Navigation über Tabulatortasten oder Selektion von Spalten oder Zeilen.
Für JTable gibt es einen Konstruktor, der ein zweidimensionales Feld annimmt und die Tabelle dann darstellt. Für uns fällt dabei wenig Arbeit an. Das 2D-Feld kann sich aus Object[][] oder auch aus Vektoren von Vektoren zusammensetzen. Intern wird ein Objektfeld jedoch in Vektoren kopiert.
Beispiel Gewünscht ist eine Tabelle mit zwei Spalten aus Strings.
String data[][] = { {"A", "B" }, { "U", "V" } };
JTable table = new JTable( data );
|
Tabelle in einer JScrollPane
Reicht der Platz für die Tabelle im Container nicht aus, so ist es sinnvoll, die Tabelle in eine JScrollPane zu setzen. Auch dann werden erst die Köpfe für die Tabelle angezeigt. Möchten wir die Spaltennamen extra setzen, so geben wir im Konstruktor einen zweiten Parameter mit den Namen an.
Hier klicken, um das Bild zu Vergrößern
Beispiel Eine Tabelle mit Überschriften in einer JScrollPane
Listing 15.35 SimpleTable.java
package table;
import javax.swing.*;
public class SimpleTable
{
public static void main( String args[] )
{
String rowData[][] = {
{ "Japan", "245" }, { "USA", "240" }, { "Italien", "220" },
{ "Spanien", "217" }, {"Türkei", "215"} ,{"England", "214"},
{ "Frankreich", "190" }, {"Griechenland", "185" },
{ "Deutschland", "180" }, {"Portugal", "170" }
};
String columnNames[] = {
"Land", "Durchschnittliche Sehdauer pro Tag in Minuten"
};
JTable table = new JTable( rowData, columnNames );
JFrame frame = new JFrame();
frame.getContentPane().add( new JScrollPane(table) );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.pack();
frame.setVisible( true );
}
}
|
15.26.1 Ein eigenes Modell
JTable ist ein gutes Beispiel für die Trennung von Daten und Anzeige. Während View und Controller in der Klasse JTable liegen, werden die Daten im Modell durch die Schnittstelle TableModel beschrieben. Jeder Datencontainer muss daher TableModel implementieren und der Anzeige eine Möglichkeit geben, Einträge in einer Zeile und Spalte zu erfragen. Ändert sich das Modell, muss zusätzlich die Visualisierung aktualisiert werden. Daher schreibt TableModel einen TableModelListener vor, der die Beobachtung übernimmt.
interface javax.swing.table.TableModel
|
|
Class getColumnClass( int columnIndex )
Liefert das allgemeinste Klassenobjekt, welches die Spalte beschreiben kann. |
|
int getColumnCount()
Liefert die Anzahl Spalten. |
|
String getColumnName( int columnIndex )
Gibt den Namen der Spalte columnIndex zurück. |
|
int getRowCount()
Liefert die Anzahl der Zeilen. |
|
Object getValueAt( int rowIndex, int columnIndex )
Gibt den Eintrag an der Stelle columnIndex und rowIndex zurück. |
|
void setValueAt( Object aValue, int rowIndex, int columnIndex )
Setzt den Wert an die gegebene Stelle. |
|
boolean isCellEditable( int rowIndex, int columnIndex )
Liefert true, wenn die Zelle an rowIndex und columnIndex editierbar ist. |
|
void addTableModelListener( TableModelListener l )
Fügt einen Ereignisbehandler hinzu, der immer dann informiert wird, wenn Daten geändert werden. |
|
void removeTableModelListener( TableModelListener l )
Entfernt den Ereignisbehandler. |
15.26.2 AbstractTableModel
Für TableModel gibt es schon eine Implementierung als abstrakte Klasse, die uns die Aufgabe abnimmt, uns um die Listener zu kümmern. Die Klasse heißt AbstractTableModel und gibt für einige Methoden eine Standardimplementierung vor. AbstractTableModel bietet Zugriff auf die Listener über eine protected-Variable listenerList.
Um ein lauffähiges Model zusammenzubauen, muss nur noch getColumnCount(), getRowCount() und getValueAt() implementiert werden, dann ist eine Modellklasse komplett. setValueAt() ist in AbstractTableModel leer implementiert und muss nur bei editierbaren Datenmodellen angepasst werden. isCellEditable() liefert false und muss bei editierbaren Modellen ebenso überschrieben werden. getColumnName() liefert Spaltennamen nach dem Muster A, B, C, ... Z, AA, AB. getColumnClass() liefert Object.class. Um nach einer Spalte suchen zu können, gibt findColumn(String) den Index der Spalte zurück, die den eingetragenen Namen hat.
Beispiel Wenn wir eine Tabelle mit Quadrat und Kubik nutzen, dann können wir ein Modell implementieren, das in der ersten Spalte die Zahl, in der zweiten das Quadrat und in der dritten das Kubik abbildet. Die Tabelle besitzt damit drei Spalten. Sie soll zehn Zeilen groß sein.
|
Hier klicken, um das Bild zu Vergrößern
Listing 15.36 QuadratTable.java, QuadratTableModelSimple
package table;
class QuadratTableModelSimple extends AbstractTableModel
{
public int getRowCount()
{
return 10;
}
public int getColumnCount()
{
return 3;
}
public Object getValueAt( int row, int col )
{
if ( col == 0 )
return ""+row;
else if ( col == 1 )
return ""+(row*row);
else
return ""+(row*row*row);
}
}
Hier klicken, um das Bild zu Vergrößern
Abbildung 15.24 JTable mit Model
Ereignisse bei Änderungen
Die Events, die AbstractTableModel auslöst, sind vom Typ TableModelEvent und werden von fireTableDataChanged(), fireTableStructureChanged(), fireTableRowsInserted(), fireTableRowsUpdated(), fireTableRowsDeleted(), fireTableCellUpdated() über die allgemeine Methode fireTableChanged(TableModelEvent) behandelt. Die Methoden zur Ereignisbehandlung sind damit vollständig und müssen von Unterklassen nicht mehr überschrieben werden, es sei denn, wir wollten zusätzliche Dinge in einer fire()-Methode realisieren.
Beispiel Ändern sich die Daten, muss die Visualisierung erneuert werden. Dann sollte fireTableCellUpdated() aufgerufen werden, wie für die setValueAt()-Methode gezeigt wird.
|
public void setValueAt( Object val, int row, int column )
{
foo[row][column] = aValue;
fireTableCellUpdated( row, column );
}
Die Methode fireTableCellUpdated(int, int) ist nur eine Abkürzung für Folgendes:
public void fireTableCellUpdated(int row, int column) {
fireTableChanged(new TableModelEvent(this, row, row, column));
}
15.26.3 DefaultTableModel
Kommt ein JTable ohne eigenes Modell zum Einsatz, nutzt die Implementierung eine Unterklasse von AbstractTableModel, welche sich DefaultTableModel nennt. Sie ist nicht abstrakt und könnte auch von uns verwendet werden. Nützliche Ergänzungen sind Methoden, damit an beliebiger Stelle Zellen eingetragen, verschoben und gelöscht werden können. Nutzen wir JTable ohne eigenes Modell, so verwendet es standardmäßig DefaultTableModel mit einer Implementierung von Vektoren aus Vektoren. Ein Hauptvektor speichert Vektoren für jede Zeile. Die Technik lässt sich gut an einer Methode ablesen, die ein Wert erfragt:
public Object getValueAt( int row, int column )
{
Vector rowVector = (Vector)dataVector.elementAt( row );
return rowVector.elementAt( column );
}
Mit den Methoden setDataVector() und getDataVector() lassen sich die Daten intern setzen und auslesen. Diese interne Abbildung der Daten ist jedoch nicht immer gewünscht, da dynamische Strukturen von der Laufzeit her ineffizient sein können. Ist das zu unflexibel, lässt sich immer noch ein eigenes Modell von AbstractTableModel ableiten.
Das Modell nutzen
Besitzen wir eine Klasse, die ein TableModel implementiert, etwa eine Unterklasse von AbstractTableModel oder DefaultTableModel, so können wir ein JTable mit diesem Modell anlegen. Dafür gibt es zwei Möglichkeiten: im Konstruktor das Modell angeben oder es nachträglich mit setModel() zuweisen.
Beispiel Das QuadratTableModelSimple soll unserer Tabelle zugewiesen werden.
TableModel model = new QuadratTableModelSimple();
JTable table = new JTable();
table.setModel( model );
|
Hier klicken, um das Bild zu Vergrößern
15.26.4 Ein eigener Renderer für Tabellen
Damit eine Tabelle nicht nur die typischen Informationen in Zeichenketten darstellen muss, lässt sich ein TableCellRenderer einsetzen, mit dem sich die Tabelleneinträge beliebig visualisieren lassen. Die Schnittstelle TableCellRenderer schreibt nur eine Methode vor.
interface javax.swing.table.TableCellRenderer
|
|
Component getTableCellRendererComponent( JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) |
|
Die Informationen über isSelected, hasFocus, row und column sollen der Zeichenfunktion die Möglichkeit geben, ausgewählte Tabellenteile besonders zu behandeln. Steht etwa auf einer Zelle der Fokus, ist ein Rahmen gezeichnet. Ist die Tabelle selektiert, ist die Zelle mit einer Hintergrundfarbe ausgeschmückt. |
DefaultTableCellRenderer
Glücklicherweise bietet Swing wieder eine Standardimplementierung in Form der Klasse DefaultTableCellRenderer. Diese erweitetert JLabel. Damit lässt sich schon viel anfangen. Das Ändern des Textes ist genauso einfach wie das Ändern der Farbe oder das Hinzufügen eines Bilds. Viele Aufgaben sind so schon erledigt. Wenn es aufwändiger realisiert werden soll, dann müssen wir direkt TableCellRenderer implementieren.
Hier klicken, um das Bild zu Vergrößern
Für unsere Zwecke soll DefaultTableCellRenderer genügen. Die wichtigste Methode zum Überschreiben ist setValue(Object). In DefaultTableCellRenderer sieht die Originalmethode wie folgt aus:
protected void setValue( Object value ) {
setText( (value == null) ? "" : value.toString() );
}
Da JTable diesen Renderer als Standard nutzt, sagt es aus, dass alle Daten in der Tabelle als String-Repräsentation eingesetzt werden.
Wenn wir eigene Visualisierungen wünschen, zum Beispiel mit einer anderen Schriftfarbe, so überschreiben wir einfach setValue() und setzen den Text mit setText() selbst.
Beispiel In einer Tabelle befinden sich nur Zahlen. Ist eine Zahl negativ, so soll sie rot erscheinen.
public void setValue( Object value )
{
String s = value.toString();
if ( Integer.parseInt( s ) < 0 )
setForeground( Color.red );
setText( s );
}
|
Die günstige Eigenschaft, dass DefaultTableCellRenderer eine Unterklasse von JLabel ist, macht sich bei setForeground() bemerkbar. Für mehrzeiligen Text machen sich die Unterklassen von JText ganz gut.
Liegen im Modell einer JTable nicht nur Daten einer Gattung, so lassen sie sich mit instanceof aufschlüsseln.
Beispiel In einer Tabelle sollen Zahlen (etwa vom Typ Integer) und Objekte vom Typ Gfx liegen. Gfx-Objekte enthalten ein Icon-Objekt mit dem Namen icon. Es soll in die Tabelle gesetzt werden.
public void setValue( Object value )
{
if ( value instanceof Gfx ) {
Gfx gfx = (IconData)value;
setIcon( gfx.icon );
}
else
super.setValue( value );
}
}
|
Die Behandlung im else-Zweig ist dabei sehr wichtig, denn dort wird der Rest der Daten behandelt. Ist es Text, so kümmert sich die Implementierung von DefaultTableCellRenderer darum. Bei setIcon() profitieren wir wieder von der Erweitung von JLabel.
Beispiel Unserer Tabelle mit den Quadrat- und Kubikzahlen wollen wir einen Renderer mitgeben. Er soll die geraden Zahlen in Blau anzeigen und die ungeraden in Grau.
Listing 15.37 ColoredTableCellRenderer.java
package table;
import java.awt.*;
import javax.swing.table.*;
class ColoredTableCellRenderer extends DefaultTableCellRenderer
{
public void setValue( Object value )
{
if ( value instanceof Long )
{
if ( ((Long)value).longValue()%2 == 0 )
setForeground( Color.blue );
else
setForeground( Color.gray );
setText( ""+value );
}
else
super.setValue( value );
}
}
|
Renderer zuweisen
Um diesen zuzuweisen, verbinden wir einen Datentyp einschließlich Renderer mit der Methode setDefaultRenderer() von JTable.
DefaultTableCellRenderer ren = new ColoredTableCellRenderer();
table.setDefaultRenderer( Long.class, ren );
Mehrzeilige Tabellenzellen
Der DefaultTableCellRenderer ist eine Unterklasse von JLabel. Diese unterstützt mehrzeilige Textfelder nicht direkt. Eine Lösung wäre jedoch, die HTML-Darstellung einzuschalten, im Text also etwa <HTML>Zeile1<BR>Zeile2</HTML> zu schreiben. Eine andere Möglichkeit ist es, einen eigenen Renderer zu implementieren, der nicht von DefaultTableCellRenderer abgeleitet ist. Eine gute Lösung ist, JTextArea als Oberklasse zu nutzen und die notwendige Schnittstelle TableCellRenderer zu implementieren. Die implementierte Methode getTableCellRendererComponent() liefert dann das this-Objekt zurück, gesetzt mit dem Text inklusive Zeilenumbruch.
Listing 15.38 table/DoubleTeamTextCellRenderer.java
package table;
import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
public class DoubleTeamTextCellRenderer
extends JTextArea implements TableCellRenderer
{
public Component getTableCellRendererComponent( JTable table,
Object value, boolean isSelected, boolean hasFocus,
int row, int column )
{
// Text setzen
setText( "1\n2" );
return this;
}
}
15.26.5 Spalteninformationen
Alle Zelleninformationen der Tabelle stecken im Modell einer JTable. Doch Informationen über die Spalten werden nicht im TableModel abgespeichert, sondern in Objekten vom Typ TableColumn. Jede Spalte bekommt ein eigenes TableColumn-Objekt, und eine Sammlung der Objekte bildet das TableColumnModel, welches wie das TableModel ein Datencontainer der JTable ist.
Beispiel Zähle alle TableColumn-Objekte einer JTable table auf.
for ( Enumeration enum = table.getColumnModel().getColumns();
enum.hasMoreElements(); )
System.out.println( (TableColumn)enum.nextElement() );
|
getColumns() bezieht eine Enumeration von TableColumn-Objekten. Soll ein ganz bestimmtes TableColumn untersucht werden, so kann auch die Funktion getColumn(index) genutzt werden. Liegt ein TableColumn vor, so lässt sich von diesem die aktuelle minimale und maximale Breite setzen.
Beispiel Ändere von der ersten Spalte die Breite auf 100 Pixel.
TableColumn col = table.getColumnModel().getColumn( 0 );
col.setPreferredWidth( 100 );
|
15.26.6 Tabellenkopf von Swing-Tabellen
Der Kopf (engl. header) einer JTable ist ein JTableHeader-Objekt, welches von der JTable mit getTableHeader() erfragt werden kann. Dieses JTableHeader-Objekt ist für die Anordnung und Verschiebung der Spalten verantwortlich. Diese Verschiebung kann über das Programm erfolgen (moveColumn()) oder über den Benutzer per Drag&Drop.
Beispiel In der JTable table sollen die Spalten nicht mehr vom Benutzer verschoben werden können. Er soll auch ebenfalls die Breite nicht mehr ändern dürfen.
table.getTableHeader().setReorderingAllowed( false );
table.getTableHeader().setResizingAllowed( false );
|
Hier wird deutlich, dass ein JTableHeader die Steuerung der Ausgabe und der Benutzerinteraktion übernimmt, aber in TableColumn die Informationen selbst liegen.
15.26.7 Selektionen einer Tabelle
In einer JTable können auf unterschiedliche Art und Weise Zellen selektiert werden: zum einen nur in einer Zeile oder Spalte, dann einmal ein ganzer Block oder auch beliebige Zellen. Die Art der Selektion bestimmen Konstanten in ListSelectionModel. So wird SINGLE_SELECTION nur die Selektion einer einzigen Zelle zulassen.
Beispiel In einer JTable sollen entweder ein ununterbrochener Block Zeilen oder Spalten ausgewählt werden dürfen:
table.setSelectionMode( ListSelectionModel.SINGLE_INTERVAL_SELECTION );
|
In Abhängigkeit eines gültigen Selektionsmodus lassen sich über Methoden alle Elemente einer Spalte oder Zeile selektieren. Das Selektieren erlauben jedoch erst zwei Funktionen.
table.setColumnSelectionAllowed( boolean );
table.setRowSelectionAllowed( boolean );
Die Selektion von Spalten gelingt mit setColumnSelectionInterval(), weitere Bereiche lassen sich mit addColumnSelectionInterval() hinzufügen und mit removeColumnSelectionInterval() entfernen. Das Gleiche gilt für die Methoden, die Row im Methodennamen tragen.
Schauen wir uns einige Beispiele an: Selektiere in einer JTable table Spalte 0 komplett.
table.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION );
table.setColumnSelectionAllowed( true );
table.setRowSelectionAllowed( false );
table.setColumnSelectionInterval( 0, 0 );
Selektiere in einer Tabelle nur die Zelle 38,5.
table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
table.setColumnSelectionAllowed( true );
table.setRowSelectionAllowed( true );
table.changeSelection( 38, 5, false, false );
Als Selektionsmodus reicht SINGLE_SELECTION aus, MULTIPLE_INTERVAL_SELECTION wäre aber auch in Ordnung. Beide Selektionen sind zusammen in der Form nicht möglich. Bei einer Einzelselektion wird die Zelle nur umrandet, aber nicht wie beim Standard-Metal-Look&Feel blau ausgefüllt.
Die Methode selectAll() selektiert alle Elemente, clearSelection() löscht alle Selektionen.
|