JTable (Tutorial)©

Aus Byte-Welt Wiki
Zur Navigation springenZur Suche springen

© August 2004 Dieses Tutorial unterliegt dem Copyright, Kopien (auch nur von Teilen) sind nur für nicht-komerzielle Zwecke gestattet; Detailfragen (insbesondere bei Unsicherheiten), bitte direkt an die Autoren.

Einführung

Von allen Components in ganz AWT und Swing, ist das JTable wohl die komplexeste überhaupt (allenfalls kann der JTree noch mithalten).

Ziel ist es, dem Leser einen Überblick zu verschaffen, und ihn an die wichtigsten Konzepte heranzuführen. Dabei soll hier nur jedes Themengebiet für sich alleine betrachtet werden, die Kombination aller TableModels, TableCellRenderer, JTableHeaders, TableColumnModels und TableCellEditors können hier nicht behandelt werden, da wir schliesslich kein Buch schreiben wollen.

Dieses Tutorial ist eigentlich eine grosse Sammlung von Beispielen. Am meisten lernt man immer noch, wenn man selbst rumspielen darf, deshalb: kopiere alle Beispiele und führ sie aus. Verändere ein paar Werte, und schau was passiert. Versuch selbst was sinnvolles zu schreiben.

Dabei konnte nicht immer die beste Implementation gewählt werden, wiel dies das Konzept (von vorne beginnen) gründlich durcheinander gebracht hätte.

1. Die JTable

Sollte diese Component wirklich unbekannt sein, von dem hier sprechen wir:
JTableTutorial1.png

2. API

Anlaufstelle Nr. 1 ist, wie so oft, die API JTable. JTable findet man im Package javax.swing, und eine ganze Reihe von hilfreichen Klassen und Interfaces im Unterpackage javax.swing.table.

3. Tutorials und anderes im WWW

- Das How to use JTable von Oracle bietet einen einfachen Überblick. - Javatip 116 bietet ebenfalls eine kleine Einführung. - Javatip 102 beschäftigt sich mit dem CellEditor, insbesondere mit verschiedenen CellEditors in derselben Column. - Ein weiteres Tutorial und eine thematische Sortierung von Methoden, etc... findet sich in dem Handout des Kurses Human Computer Interaction - Die jDynTable ist eine Erweiterung der normalen JTable, und erlaubt auch verschmolzene Zellen (Zellen über mehrere Spalten).

4. Scrolling

Keine Tabelle wäre vollständig ohne Scrollbars und Titelleiste (JTableHeader). Diese Funktionalität erhält man erst, wenn man die JTable mit einem JScrollPane kombiniert. Das macht man am besten mit dem Konstruktor der JScrollPane:

// Das JTable
JTable table = ...

// Der Container, dem das JTable hinzugefügt wird
Container content = ...

// Ein JScrollPane erstellen, und zwischen dem Table und dem Container einfügen.
content.add( new JScrollPane( table ));

Für diejenigen die einfache Lösungen nicht gerne haben, mit getTableHeader kommt man direkt an die Titelliste und kann sie irgendwohin schieben:

// dem Container das [[BorderLayout]] setzen
content.setLayout(new BorderLayout());

// den Header der JTable holen und ihn in den oberen Bereich des Containers setzen
content.add(table.getTableHeader(), BorderLayout.PAGE_START);

// das JTable in die Mitte des Containers setzen
content.add(table, BorderLayout.CENTER);

Man kann unendlich viele Dinge mit einer JTable tun, aber nicht Alles ist sinnvoll. Im letzten Beispiel hat der Benutzer keine möglichkeit die Tabelle umherzuschieben und verdeckte Zellen in den Sichtbarkeitsbereich zu scrollen.

Wie kommen Daten in die Tabelle?

JTable selbst hat nur die Aufgabe Daten hübsch darzustellen. Die Tabelle versteht weder woher die Daten kommen, noch was die Daten bedeuten. Wie also kommen die Daten in die Tabelle?

1. Möglichkeit - 2d Array

Die wohl einfachste Methode ist einen 2-dimensionalen Array zu benutzen. Das JTable bietet einen entsprechenden Konstruktor:

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class JTableDemo{
	public static void main( String[] args ){
		// Die Daten für das Table
		String[][] data = new String[][]{
				{"a", "b", "c", "d"},
				{"e", "f", "g", "h"},
				{"i", "j", "k", "l"}
		};
		
		// Die Column-Titles
		String[] title = new String[]{
				"A", "B", "C", "D"
		};
		
		// Das JTable initialisieren
		JTable table = new JTable( data, title );
		
		JFrame frame = new JFrame( "Demo" );
		frame.getContentPane().add( new JScrollPane( table ) );
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.pack();
		frame.setVisible( true );
	}
}

JTableTutorial2.png Diese Methode ist sehr statisch, es ist später nicht mehr gut möglich, Daten zu ändern.

2. Möglichkeit - Vectoren

Mit ineinander gesetzten Vektoren:

import java.util.Vector;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;

public class JTableDemo{
	public static void main( String[] args ){
		// Die Daten für das Table
		Vector data = new Vector();
			Vector rowA = new Vector();
				rowA.add(  "1" );
				rowA.add(  "2" );
				rowA.add(  "3" );
				rowA.add(  "4" );
			Vector rowB = new Vector();
				rowB.add(  "5" );
				rowB.add(  "6" );
				rowB.add(  "7" );
				rowB.add(  "8" );
			Vector rowC = new Vector();
				rowC.add(  "9" );
				rowC.add( "10" );
				rowC.add( "11" );
				rowC.add( "12" );
		
			data.add( rowA );
			data.add( rowB );
			data.add( rowC );
				
		// Die Titel für das Table
		Vector title = new Vector();
			title.add( "A" );
			title.add( "B" );
			title.add( "C" );
			title.add( "D" );
		
		// Das JTable initialisieren
		JTable table = new JTable(  data, title );
		
		JFrame frame = new JFrame( "Demo" );
		frame.getContentPane().add( new JScrollPane( table ) );
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.pack();
		frame.setVisible( true );
	}
}

JTableTutorial3.png

Das hat natürlich den Vorteil, dass man die Vektoren an verschiedenen Orten zusammenbauen kann, doch auch hier gilt: nachdem das JTable initialisiert wurde, kann man nichts mehr verändern.

3. Möglichkeit - DefaultTableModel

Mit einem DefaultTableModel.

Ein DefaultTableModel kann man sich auch als 2-dimensionalen Array vorstellen. Allerdings besitzt es Methoden, um neue Rows/Columns hinzuzufuegen, oder zu entfernen, z.B. addRow.

Was ein TableModel ist, wird im naechsten Teil behandelt.

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

public class JTableDemo{
	public static void main( String[] args ){
		// Die Namen der Columns
		String[] titles = new String[]{ "A", "B", "C", "D" };
		
		// Das Model das wir verwenden werden. Hier setzten wir gleich die
		// Titel, aber es ist später immer noch möglich weitere Columns oder
		// Rows hinzuzufügen.
		final DefaultTableModel model = new DefaultTableModel( titles, 0 );
		
		// Das JTable initialisieren
		JTable table = new JTable( model );
		
		// Buttons, damit das alles schöner aussieht.
		final JButton buttonAddRow = new JButton( "add row" );
		final JButton buttonRemRow = new JButton( "remove row" );
		final JButton buttonAddCol = new JButton( "add column" );
		
		buttonRemRow.setEnabled( false );
		
		// Den Buttons ein paar Reaktionen geben
		buttonAddRow.addActionListener( new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				// Die Anzahl Columns (Breite) der Tabelle
				int size = model.getColumnCount();
				
				// einen neuen Vector mit Daten herstellen
				Vector newDatas = createDataVector( "row", size );
				
				// eine neue Row hinzufügen
				model.addRow( newDatas );
				
				
				// das Entfernen erlauben
				buttonRemRow.setEnabled( true );
			}
		});
		
		buttonAddCol.addActionListener( new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				int size = model.getRowCount();
				Vector newDatas = createDataVector( "column", size );
				String name = String.valueOf( model.getColumnCount() );
				model.addColumn( name, newDatas );
			}
		});
		
		buttonRemRow.addActionListener( new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				int size = model.getRowCount();
				int index = (int)(Math.random() * size);
				model.removeRow( index );
				
				buttonRemRow.setEnabled( size > 1 );
			}
		});
		
		JFrame frame = new JFrame( "Demo" );
		
		Container content = frame.getContentPane();
		
		content.add( new JScrollPane( table ), BorderLayout.CENTER );
		content.add( buttonAddRow, BorderLayout.NORTH );
		content.add( buttonRemRow, BorderLayout.SOUTH );
		content.add( buttonAddCol, BorderLayout.WEST );
		
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.pack();
		frame.setVisible( true );
	}
	
	public static Vector createDataVector( String prefix, int size ){
		Vector vector = new Vector( size );
		for( int i = 0; i < size; i++ )
			vector.add( prefix + " : " + size + " : " + i );
		
		return vector;
	}
}

JTableTutorial4.png

Was geschieht hier? Es gibt 3 JButtons, welche jeweils eine Row oder Column hinzufügen, oder eine Row entfernen. Die seltsame Schreibweise addActionListener( ... ) stellen anonyme Klassen dar.

Die eigentliche Hauptaktion macht das DefaultTableModel "model", alles andere dient nur dazu, dem Benutzer eine Möglichkeit zu bieten mit der Tabelle zu spielen.

4. Möglichkeit - TableModel

Mit Hilfe eines TableModels. Das TableModel ist ein Interface. Das JTable ruft über die Methode TableModel#getValueAt( int rowIndex, int columnIndex) für jede einzelne Zelle den Wert ab. Das muss nicht in einer bestimmten Reihenfolge geschehen, das JTable nimmt einfach gerade das, was es benötigt.

Das folgende kleine Beispiel zeigt: die Klasse Vehicle, die eigentlich überhaupt nichts mit einer Tabelle zu tun hat, kann mit Hilfe eines TableModels auf die JTable gebracht werden. Wichtig dabei ist: man muss nicht zuerst "von Hand" die Daten vorsortieren, sondern man gibt sie einfach dann zurück, wenn sie auch tatsächlich benötigt werden.

Was ist nun, falls die Daten veraendert wurden (z.B. eine Row hinzugefuegt)? Man muss die Methoden #addTableModelListener und #removeTableModelListener implementieren. Diese Methoden werden von verschiedenen Orten aufgerufen, zum Beispiel von JTable selbst, aber mögleicherweise auch vom Look And Feel. All diese Listener interessieren sich fuer Veränderungen des Models. Sobald sich die Daten ändern, muss man bei allen Listenern die Methode tableChanged( TableModelEvent event) aufrufen.

Das TableModelEvent enthält alle Informationen über die Veränderungen. Die Werte für das Event können nur über den Konstruktor gesetzt werden, ein paar einfache Beispiele aus der API:

// source Das TableModel, welches verändert wurde

TableModelEvent(source); // Sämtliche Daten von source wurden verändert
TableModelEvent(source, TableModelEvent.HEADER_ROW); // Die Struktur der Daten wurde verändert, die Columns werden auf den Ursprungszustand gesetzt. 
TableModelEvent(source, 1); // Row 1 wurde verändert
TableModelEvent(source, 3, 6); // Row 3, 4, 5, 6 wurden verändert
TableModelEvent(source, 2, 2, 6); // Die Zelle (2, 6) wurde verändert
TableModelEvent(source, 3, 6, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT); // Es wurden neue Rows hinzugefügt, sie haben nun die Indices 3, 4, 5 und 6
TableModelEvent(source, 3, 6, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE); // Die Rows mit den Indices 3, 4, 5 und 6 wurden gelöscht.

TableModelEvent(source, 2, 3, 5, TableModelEvent.INSERTED ) // Fehler: Es ist nicht möglich, dass Zelle (2,5) und (3,5) hinzugefügt werden.

Sicher bekannt ist das Verhältnis zwischen Button und ActionListener. Sobald jemand den Button drückt, wird der ActionListener aufgerufen. Der Button ist sozusagen für die Aktion, der ActionListener für die Reaktion zuständig. Hier ist es genau umgekehrt: das TableModel startet die Aktion, die TableModelListener reagieren darauf.

Das folgende Beispiel zeigt, wie man dem JTable (indirekt) sagen kann, dass eine neue Row hinzugefügt wurde. Genau gleich wie hier das Hinzufügen von neuen Vehikeln, kann man auch das Löschen von einzelnen Columns, die Veränderung des Inhalts einer Zelle, etc... implementieren.

mehr zu Listeners.

Zuerst das JFrame, wie wir es verwenden möchten:

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

public class JTableDemo{
	public static void main( String[] args ){
		// Unser TableModel (siehe unten)
		final Model model = new Model();
		
		// Das JTable initialisieren
		JTable table = new JTable( model );
		
		// Buttons, damit das alles schöner aussieht.
		final JButton buttonVehicle = new JButton( "add vehicle" );
		
		// Den Buttons ein paar Reaktionen geben
		buttonVehicle.addActionListener( new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				// Die Anzahl Columns (Breite) der Tabelle
				int size = model.getRowCount();
				
				// einen neuen Vector mit Daten herstellen
				Vehicle vehicle = createVehicle( size );
				
				// ein neues Vehikel hinzufügen
				model.addVehicle( vehicle );
			}
		});
				
		JFrame frame = new JFrame( "Demo" );
		
		Container content = frame.getContentPane();
		
		content.add( new JScrollPane( table ), BorderLayout.CENTER );
		content.add( buttonVehicle, BorderLayout.SOUTH );
		
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.pack();
		frame.setVisible( true );
	}
	
	// Stellt einfach eine neue Instanz eines Vehikels her.
	public static Vehicle createVehicle( int index ){
		index = index % 5; // Modulo
		
		switch( index ){
			case 0: return new Vehicle( "Fahrrad", 1, 2, false );
			case 1: return new Vehicle( "Bus", 20, 4, true );
			case 2: return new Vehicle( "Pferd", 1, 0, false );
			case 3: return new Vehicle( "Zug", 1000, 80, true );
			case 4: return new Vehicle( "Truck", 2, 10, true );
			default: return null;	
		}
		
	}
}

Der Code für eine Datenstruktur "Vehicle":

// Das Vehikel ist eine total unabhängige Klasse, die mit einer
// Tabelle eigentlich gar nichts zu tun hat.
class Vehicle{
	private String name;
	private int places, wheels;
	private boolean motor;
	
	public Vehicle( String name, int places, int wheels, boolean motor ){
		this.name = name;
		this.places = places;
		this.wheels = wheels;
		this.motor = motor;
	}
	
	public String getName(){ return name; }
	public int getPlaces(){ return places; }
	public int getWheels(){ return wheels; }
	public boolean hasMotor(){ return motor; }
}

Das Model, dass die Vehicle in eine Form übersetzt, welche das JTable anzeigen kann:

// Unsere Implementation des TableModels
class Model implements TableModel{
	private Vector vehicles = new Vector();
	private Vector listeners = new Vector();
	
	public void addVehicle( Vehicle vehicle ){
		// Das wird der Index des Vehikels werden
		int index = vehicles.size();
		vehicles.add( vehicle );
		
		// Jetzt werden alle Listeners benachrichtigt
		
		// Zuerst ein Event, "neue Row an der Stelle index" herstellen
		TableModelEvent e = new TableModelEvent( this, index, index, 
				TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT );
		
		// Nun das Event verschicken
		for( int i = 0, n = listeners.size(); i<n; i++ ){
			((TableModelListener)listeners.get( i )).tableChanged( e );
		}
	}
	
	// Die Anzahl Columns
	public int getColumnCount() {
		return 4;
	}
	
	// Die Anzahl Vehikel
	public int getRowCount() {
		return vehicles.size();
	}
	
	// Die Titel der einzelnen Columns
	public String getColumnName(int column) {
		switch( column ){
			case 0: return "Name";
			case 1: return "Fahrgäste";
			case 2: return "Räder";
			case 3: return "Besitzt Motor";
			default: return null;
		}
	}
	
	// Der Wert der Zelle (rowIndex, columnIndex)
	public Object getValueAt(int rowIndex, int columnIndex) {
		Vehicle vehicle = (Vehicle)vehicles.get( rowIndex );
		
		switch( columnIndex ){
			case 0: return vehicle.getName();
			case 1: return new Integer( vehicle.getPlaces() );
			case 2: return new Integer( vehicle.getWheels() );
			case 3: return vehicle.hasMotor() ? Boolean.TRUE : Boolean.FALSE; 
			default: return null;
		}
	}

	// Eine Angabe, welchen Typ von Objekten in den Columns angezeigt werden soll
	public Class getColumnClass(int columnIndex) {
		switch( columnIndex ){
			case 0: return String.class;
			case 1: return Integer.class;
			case 2: return Integer.class;
			case 3: return Boolean.class; 
			default: return null;
		}	
	}
	
	public void addTableModelListener(TableModelListener l) {
		listeners.add( l );
	}
	public void removeTableModelListener(TableModelListener l) {
		listeners.remove( l );
	}
	

	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return false;
	}
	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
		// nicht beachten
	}
}

JTableTutorial5.png

API - TableModel

Ein Überblick aller Methoden, die das Interface TableModel vorschreibt.

int getColumnCount() Gibt die Anzahl Columns (Spalten) zurück. Dieser Wert muss natürlich >= 0 sein.

String getColumnName(int columnIndex) Gibt den Namen einer Column zurück. Der Name wird als Titel im JTableHeader verwendet.

int getRowCount() Gibt die Anzahl Rows (Zeilen) zurück. Wie der Wert bei "getColumnCount" muss dieser Wert >= 0 sein.

Object getValueAt(int rowIndex, int columnIndex) Gibt den Wert der Zelle (rowIndex, columnIndex) zurück. Dieses Object wird dem TableCellRenderer (siehe nächstes Kapitel) übergeben. Der Renderer ist eine Component, welche das Object irgendwie darstellen kann (z.B. ein JLabel das einen String als Text anzeigt.)

Primitive Datentypen wie int, double, ... werden am einfachsten in ihrere Wrapperklasse Integer, Double, ... zurückgegeben.

Das Object das hier zurückgegeben wird, muss eine Instanc von der Klasse sein, die bei #getColumnClass zurückgegeben wird.

Class getColumnClass(int columnIndex) Gibt die Klasse (oder Superklasse) aller Zellenwerte der Column columnIndex zurück. Das JTable verwendet diese Klasse, um herauszufinden, wie es diese Column am besten darstellt. z.B. Werden Icons anders dargestellt als Booleans...

z.B. die Klasse von String kann man so herausfinden:

Class string = String.class;

boolean isCellEditable(int rowIndex, int columnIndex) Gibt an, ob diese Zelle editiert werden kann. Falls ja, kann das JTable einen Editor (eine andere Component) in dieser Zelle darstellen, sollte der Benutzer sie selektieren (es gibt also immer höchstens einen sichtbaren Editor pro JTable). Wird das Editieren beendet (z.B. durch einen Druck auf ENTER), gibt der Editor ein Object zurueck, welches mit #setValueAt an das TableModel übertragen wird.

void setValueAt(Object aValue, int rowIndex, int columnIndex) Setzt an einer Stelle der Tabelle einen neuen Wert. Dieser neue Wert wird durch den Benutzer eingegeben. Diese Methode wird niemals aufgerufen, wenn #isCellEditable( rowIndex, columnIndex ) false zurueckgibt.

Sollte dem JTable nichts Spezielles gesagt worden sein, und gibt getColumnClass( columnIndex ) weder String, noch Boolean, Integer,..., oder Date zurück, wird aValue ein String sein. Dann muss das TableModel diesen String selbst übersetzen.

void addTableModelListener(TableModelListener l) Registriert einen TableModelListener bei diesem Model. Dieser Listener muss immer dann aufgerufen werden, sobald die Daten im Model verändert wurden (z.B. eine zusätzliche Row hineingeschoben wurde). Listener können vom JTable oder von anderen Klassen kommen, es muss das Model nicht interessieren.

void removeTableModelListener(TableModelListener l) Entfernt einen TableModelListener. z.B. muss das JTable ja nicht mehr über Veränderungen informiert werden, falls das Model vom Table entfernt wurde...

API - TableModelEvent

Das TableModelEvent befördert Informationen von einem TableModel zu seinen TableModelListenern.

Konstruktoren

Das TableModelEvent hat verschiedene Konstruktoren. Je nach dem welcher mit welchen Argumenten benutzt wurde, bekommt das Event eine andere Bedeutung.

source ist immer das TableModel, in dem etwas passiert ist.

TableModelEvent(TableModel source) Benutzen, falls die Veränderungen zu komplex für eine Beschreibung sind. Dann verliert das JTable allerdings viele Benutzereinstellungen, wie z.B. die Selektion.

TableModelEvent(TableModel source, int row) Gibt an, dass eine Row (mit dem Index row) verändert wurde. Allerdings kann row auch die Konstante HEADER_ROW sein, falls sich etwas mit der Titelleiste des JTables veraendert hat (z.B. ein Titel).

TableModelEvent(TableModel source, int firstRow, int lastRow) Gibt an, dass alle Rows von firstRow bis und mit lastRow verändert wurden.

TableModelEvent(TableModel source, int firstRow, int lastRow, int column) Gibt an, dass alle Rows von firstRow bis und mit lastRow verändert wurden. column kann der Index einer einzigen Column sein, oder aber die Konstante ALL_COLUMNS falls mehrere Columns verändert wurden.

TableModelEvent(TableModel source, int firstRow, int lastRow, int column, int type) Gibt an, dass alle Rows von firstRow bis und mit lastRow verändert wurden. column kann der Index einer einzigen Column sein, oder aber die Konstante ALL_COLUMNS falls mehrere Columns verändert wurden. type ist einer von INSERT, DELETE oder UPDATE.

Konstanten

static int ALL_COLUMNS Wird für das Argument column verwendet, und gibt an, dass alle Columns betroffen sind.

static int DELETE Wird für das Argument type verwendet, und gibt an, dass Rows gelöscht wurden.

static int HEADER_ROW Gibt an, dass die Titelleiste von irgendwelchen Veränderungen betroffen ist.

static int INSERT Wird für das Argument type verwendet, und gibt an, dass Rows eingefügt wurden.

static int UPDATE Wird für das Argument type verwendet, und gibt an, dass sich der Inhalt der Rows verändert hat.

Info - Aufruf von getValueAt

Es wurde ja schon gesagt: das JTable holt sich die Informationen in keiner bestimmten Reihenfolge. Das kann man sichtbar machen (aber Achtung, der Code nun folgt, sollte nicht so verwendet werden).

Die Tabelle

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

public class JTableDemo{
	public static void main( String[] args ){
		// Unser TableModel (siehe unten)
		final Model model = new Model(50, 60);
		
		// Das JTable initialisieren
		JTable table = new JTable( model );
		
		JFrame frame = new JFrame( "Demo" );
		frame.getContentPane().add( new JScrollPane( table ) );
		
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.pack();
		frame.setVisible( true );
	}
}

Ein TableModel das Integer ausgiebt, und mitzählt, wieoft "getValueAt" aufgerufen wurde (für jede Zelle einzeln)

class Model implements TableModel{
	private int width, height;
	
	int[][] calls;
	
	public Model( int width, int height ){
		this.width = width;
		this.height = height;
		calls = new int[ width ][ height ];
	}
	
	// Die Anzahl Columns
	public int getColumnCount() {
		return width;
	}
	
	// Die Anzahl Vehikel
	public int getRowCount() {
		return height;
	}
	
	// Die Titel der einzelnen Columns
	public String getColumnName(int column) {
		return String.valueOf( column );
	}
	
	// Der Wert der Zelle (rowIndex, columnIndex)
	public Object getValueAt(int rowIndex, int columnIndex) {
		// bei jedem Aufruf wird dieser Wert um 1 erhöht
		return new Integer( calls[ columnIndex ][ rowIndex ]++ );
	}

	// Eine Angabe, welchen Typ von Objekten in den Columns angezeigt werden soll
	public Class getColumnClass(int columnIndex) {
		return Integer.class;
	}
	
	public void addTableModelListener(TableModelListener l) {}
	public void removeTableModelListener(TableModelListener l) {}
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return false;
	}
	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {}
}

JTabelTutorial6.png Dieses TableModel zählt für jede Zelle, wie oft sie aufgerufen wurde.

Ausblick - Eingeben von Daten

Es wäre doch schön, wenn der Benutzer auch Daten eingeben könnte. Die JTable und das TableModel sind bereits darauf vorbereitet, und es ist nicht viel vonnöten, bis das Beispiel aus 4. veränderbare Vehikel besitzt:

Das Vehicle benötigt noch neue Methoden:

class Vehicle{
	... // der ganze Rest

	// Diese 4 Methoden müssen dem Vehikel noch hinzugefügt werden:
	public void setName( String name ){ this.name = name; }
	public void setPlaces( int places ){ this.places = places; }
	public void setWheels( int wheels ){ this.wheels = wheels; }
	public void setHasMotor( boolean motor ){ this.motor = motor; }
}

Die letzen beiden Methoden von "Model" müssen angepasst werden, so dass auch Daten gesetzt werden können

class Model implements TableModel{

	... // der ganze Rest

	// Die beiden letzten Methoden (siehe 4.) müssen noch verändert werden.

	// Gibt nun an, dass jede Zelle editierbar ist.
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return true;
	}

	// Wird von der JTable aufgerufen, falls in eine Zelle ein neuer Wert gesetzt werden soll
	// Praktischerweise ist "aValue" ein Objekt desselben Types, welchen wir bei
	// "public Class getColumnClass(int columnIndex)" zurückgegeben haben (Also String, Integer oder Boolean)
	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
		Vehicle vehicle = (Vehicle)vehicles.get( rowIndex );
		
		switch( columnIndex ){
			case 0: 
				vehicle.setName( (String)aValue );
				break;
			case 1: 
				vehicle.setPlaces( ((Integer)aValue).intValue() );
				break;
			case 2: 
				vehicle.setWheels( ((Integer)aValue).intValue() );
				break;
			case 3: 
				vehicle.setHasMotor( ((Boolean)aValue).booleanValue() );
				break;
		}
	}
}

JTableTutorial7.png

Breite der Spalten

Die Breite der Spalten lässt sich über die Objekte der Klasse TableColumn, die eine Spalte der JTable repräsentiert, verändern. Dabei benutzt man nicht die Methode setWidth, sondern die Methode setPreferredWidth. Natürlich kann man die Breite der Spalten erst festlegen, nachdem man das Model mit der Anzahl Spalten übergeben hat.

1. Möglichkeit - Direkt

Man kann die TableColumns über ihren Titel abrufen.

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class JTableDemo{
   public static void main( String[] args ){
      // Die Daten für das Table
      String[][] data = new String[][]{
            {"a", "b", "c", "d"},
            {"e", "f", "g", "h"},
            {"i", "j", "k", "l"}
      };
      
      // Die Column-Titles
      String[] title = new String[]{
            "A", "B", "C", "D"
      };
      
      // Das JTable initialisieren
      JTable table = new JTable( data, title );
      
      // Das automatische Neusetzen der Grösse würde das Vorhaben, die Grösse selbst
      // zu setzen, stören.
      table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
      
      table.getColumn( "A" ).setPreferredWidth(  20 );
      table.getColumn( "B" ).setPreferredWidth(  50 );
      table.getColumn( "C" ).setPreferredWidth(  80 );
      table.getColumn( "D" ).setPreferredWidth( 110 );
      
      JFrame frame = new JFrame( "Demo" );
      frame.getContentPane().add( new JScrollPane( table ) );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.pack();
      frame.setVisible( true );
   }
}

JTableTutorial8.png

2. Möglichkeit - Mit Indices über das TableColumnModel

Nicht immer gibt es ein eindeutiges Objekt, welches eine Column identifiziert. Ein simples Beispiel: es wird zweimal derselbe Titel gewählt, oder die Titel sind dynamisch, und man weiss nicht, wann welcher Titel angezeigt wird.

In diesem Fall muss man tiefer in das JTable hineinlangen, um die Columns zu erreichen. Man muss das TableColumnModel befragen.

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumnModel;

public class JTableDemo{
   public static void main( String[] args ){
      // Die Daten für das Table
      String[][] data = new String[][]{
            {"a", "b", "c", "d"},
            {"e", "f", "g", "h"},
            {"i", "j", "k", "l"}
      };
      
      // Die Column-Titles
      String[] title = new String[]{
            "A", "A", "A", "A"
      };
      
      // Das JTable initialisieren
      JTable table = new JTable( data, title );
      
      // Das automatische Neusetzen der Grösse würde das Vorhaben, die Grösse selbst
      // zu setzen, stören.
      table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
      
      // Über das TableColumnModel kommt man an die wichtigen Daten
      TableColumnModel columnModel = table.getColumnModel();
            
      // Die einzelnen Columns ansprechen und die Grösse setzen
      columnModel.getColumn( 0 ).setPreferredWidth( 20 );
      columnModel.getColumn( 1 ).setPreferredWidth( 50 );
      columnModel.getColumn( 2 ).setPreferredWidth( 80 );
      columnModel.getColumn( 3 ).setPreferredWidth( 110 );
            
      JFrame frame = new JFrame( "Demo" );
      frame.getContentPane().add( new JScrollPane( table ) );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.pack();
      frame.setVisible( true );
   }
}

JTableTutorial8.png

3. Der Auto-Resize-Modus

In den beiden vorangegangen Beispielen wurd der Autoresize-Modus deaktiviert.

table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );

Wenn man diese Zeile weglässt, versucht das JTable die Grösse der Spalten anzupassen. Dabei benutzt es die Einstellungen der "preferredWidth", welche man ja verändert hat:

a) mit dem normalen Resize-Mode:

JTableTutorial9.png


b) zum Vergleich, mit deaktiviertem Resize-Mode:

JTableTutorial8.png

Darstellung der Daten

Bereits in im vorhergehenden Teil hat man gesehen, dass das JTable offenbar mehr kann als nur Text zu zeichnen:

Datei:JTableTutorial12.png

1. Das Standardverhalten

Mit Hilfe der Methode TableModel#getColumnClass versucht das JTable herauszufinden, wie diese Column am besten dargestellt werden kann.

Ein Blick in den Quellcode von JTable verrät, dass Number, Float, Double, Date, Icon, ImageIcon und Boolean eine Spezialbehandlung erhalten.

Sollte ein Object nicht dazu passen, wird die "toString" Methode benutzt.

Demonstrieren wir mal, wie das aussieht:

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.util.Date;

import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;

public class JTableDemo{
   public static void main( String[] args ){
      // Unser TableModel (siehe unten)
      Model model = new Model();
      
      // Das JTable initialisieren
      JTable table = new JTable( model );
      
      JFrame frame = new JFrame( "Demo" );
      frame.getContentPane().add( new JScrollPane( table ) );
      
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.pack();
      frame.setVisible( true );
   }
}

Ein einfaches Icon, damit man nicht Bilder laden, etc... muss:

class DemoIcon implements Icon{
	public int getIconHeight() {
		return 15;
	}
	public int getIconWidth() {
		return 15;
	}
	public void paintIcon(Component c, Graphics g, int x, int y) {
		g.setColor( Color.DARK_GRAY );
		g.fillOval( x, y, 10, 10 );
		g.setColor( Color.GREEN );
		g.fillOval( x+5, y+5, 10, 10 );
	}
}

Wiedereinmal ein TableModel. Diesmal wird AbstractTableModel als Basisklasse benutzt, denn so muss man weniger Methoden implementieren:

class Model extends AbstractTableModel{
	private Object[] objects;
	private Class[] classes;
	
   	public Model(){
   		objects = new Object[]{
				"Text",
				Float.valueOf( 5.123f ),
				Boolean.valueOf( true ),
				new Date( System.currentTimeMillis() ),
				new DemoIcon(),
				Color.RED
   		};
   		
   		classes = new Class[]{
				String.class,
				Float.class,
				Boolean.class,
				Date.class,
				Icon.class,
				Color.class
   		};
   	}
   
   	// Die Anzahl Columns
   	public int getColumnCount() {
   		return objects.length;
   	}
   
   	// Die Anzahl Rows
   	public int getRowCount() {
   		return 1;
   	}
   
   	// Die Titel der einzelnen Columns (Der Name der Klasse die Dargestellt wird)
   	public String getColumnName(int column) {
   		return getColumnClass( column ).getName();
   	}
   
   	// Der Wert der Zelle (rowIndex, columnIndex)
   	public Object getValueAt(int rowIndex, int columnIndex) {
   		return objects[ columnIndex ];
   	}

   	// Eine Angabe, welchen Typ von Objekten in den Columns angezeigt werden soll
   	public Class getColumnClass(int columnIndex) {
   		return classes[columnIndex];
   	}
}

JTableTutorial13.png

Wie man sieht, wurde das Color-Objekt nicht gerade schön dargestellt. Da das JTable nicht wusste, wie es mit einem Color-Objekt umgehen soll, wurde einfach die "toString"-Methode benutzt.

2. Ein einfacher TableCellRenderer

Tabellenzellen werden standardmäßig quasi von JLabel dargestellt. Ein DefaultTableCellRenderer erbt von JLabel und implementiert neben weiteren Interfaces auch TableCellRenderer, welches die Methode

public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)

einführt. In seiner einfachsten Variante braucht ein TableCellRenderer nur die setValue()-Methode zu überschreiben. Das empfiehlt sich, wenn man die o.g. Standarddatentypen, wie String, Boolean, Number usw. in ihrer Darstellung, mit den von JLabel angebotenen Möglichkeiten, anpassen möchte.

Wenn eine Zelle ein Color-Objekt darstellen soll, kann die Farbe direkt dargestellt werden:

public class ColorTableCellRenderer extends DefaultTableCellRenderer {

    @Override
    protected void setValue(Object value) {
        if (value instanceof Color) { //Wenn das TableModel für diese Zelle ein Color-Objekt zurück gibt
	    Color color = (Color)value;		
            setText( color.getRed() + ", " + color.getBlue() + ", " + color.getGreen() );
	    setBackground( color );
            setHorizontalAlignment(JLabel.CENTER);
        } else {
            super.setValue(value); //ansonsten möchten wir das Verhalten vom DefaultTableCellRenderer
        }
    }
}

JTableTutorial14.png

Eine weitere Möglichkeit besteht im Überschreiben der Methode getTableCellRendererComponent(), in der die gewünschten Manipulationen programmiert werden. Wie kann man dem JTable also nun sagen, dass Color auch speziell angezeigt werden soll? Indem man einen TableCellRenderer implementiert, und dem JTable über die Methode JTable#setDefaultRenderer übergibt.

Der Einfachheit halber, wird für das Beispiel ein DefaultTableCellRenderer genommen, und ein bisschen verändert. Die bekannte Tabelle mit einem zusätzlichen Befehl

public class JTableDemo{
   public static void main( String[] args ){
      // Unser TableModel (siehe unten)
      Model model = new Model();
      
      // Das JTable initialisieren
      JTable table = new JTable( model );
      
      // Hier wird der JTable gesagt, dass Objekte des Types "Color" vom "ColorTableCellRenderer" dargestellt werden
      table.setDefaultRenderer( Color.class, new ColorTableCellRenderer() );
      
      JFrame frame = new JFrame( "Demo" );
      frame.getContentPane().add( new JScrollPane( table ) );
      
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.pack();
      frame.setVisible( true );
   }
}

Das DemoIcon bleibt unverändert:

class DemoIcon implements Icon{
   ...
}

Auch an "Model" muss nichts verändert werden:

class Model extends AbstractTableModel{
   ...
}

Und jetzt kommt der neue TableCellRenderer.

class ColorTableCellRenderer extends DefaultTableCellRenderer{
	public Component getTableCellRendererComponent(JTable table, Object value,
			boolean isSelected, boolean hasFocus, int row, int column) {
		super.getTableCellRendererComponent(table, value, isSelected,
				hasFocus, row, column);
		
		Color color = (Color)value;
		
		setText( color.getRed() + ", " + color.getBlue() + ", " + color.getGreen() );
		setForeground( color );
		
		return this;
	}
}

JTableTutorial15.png

Das JTable erwartet von einem TableCellRenderer eine Component, welche genau eine Zelle darstellt. Allerdings wird diese Component nicht für immer behalten, sondern sie muss nur kurz diese Zelle zeichnen, und wird danach nicht mehr benötigt.

Was macht "ColorTableCellRenderer"? Sobald das JTable vom ColorTableCellRenderer eine Component haben möchte, ruft es ersteinmal die entsprechende Methode von DefaultTableCellRenderer auf. Dort werden Hintergrundfarbe und Schriftart gesetzt. Danach setzt ColorTableCellRenderer einen Text der zu der Farbe passt (die RGB-Werte), und verändert die Schriftfarbe. Und dann gibt er sich selbst zurück, denn betrachtet man die API, so sieht man, dass DefaultTableCellRenderer eine Unterklasse von JLabel (und damit auch von Component) ist, und dies deshalb ohne Probleme geht.

Wichtig! Es muss nie eine neue Component hergestellt werden, das Schlüsselwort new hat hier nichts verloren.

3. Alles zum gleichen TableCellRenderer

Manchmal möchte man vielleicht mehr Kontrolle über die Art, wie die Zellen dargestellt werden. Eine einfache Variante ist folgende: Das TableModel gibt für alle ColumnClasses an, sie seien vom Typ Object. Und für den Typ Object setzt man einen eigenen TableCellRenderer.

Die Tabelle mit anderem TableCellRenderer:

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.util.Date;

import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;

public class JTableDemo{
   public static void main( String[] args ){
      // Unser TableModel (siehe unten)
      Model model = new Model();
      
      // Das JTable initialisieren
      JTable table = new JTable( model );

      // Den Spezial-CellRenderer übergeben.
      table.setDefaultRenderer( Object.class, new Renderer() );
            
      JFrame frame = new JFrame( "Demo" );
      frame.getContentPane().add( new JScrollPane( table ) );
      
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.pack();

      frame.setVisible( true );
   }
}

Das DemoIcon bekommt noch die Möglichkeit, andere Farben zu benutzen:

class DemoIcon implements Icon{
	private Color top, bottom;
	
	public DemoIcon( Color top, Color bottom ){
		this.top = top;
		this.bottom = bottom;
	}
	
	public int getIconHeight() {
		return 15;
	}
	public int getIconWidth() {
		return 15;
	}
	public void paintIcon(Component c, Graphics g, int x, int y) {
		g.setColor( top );
		g.fillOval( x, y, 10, 10 );
		g.setColor( bottom );
		g.fillOval( x+5, y+5, 10, 10 );
	}
}

Das TableModel gibt nun für jede Column an, dass Object's angezeigt werden

private Object[][] objects;
	
   	public Model(){
   		objects = new Object[][]{
   				{"Text",
				new Float( 5.123f ),
				new Boolean( true ),
				new Date( System.currentTimeMillis() ),
				new DemoIcon( Color.DARK_GRAY, Color.GREEN ),
				Color.RED},
				
   				{"Hallo",
				new Double( 531.45 ),
				new Boolean( false ),
				new Date( System.currentTimeMillis() + 50000000 ),
				new DemoIcon( Color.BLUE, Color.GRAY ),
				Color.CYAN},
				
	   			{"Tschau",
				new Integer( 123 ),
				new Boolean( true ),
				"unbekannt",
				new DemoIcon( Color.LIGHT_GRAY, Color.MAGENTA ),
				Color.PINK}	
   		};
   	}
   
   	// Die Anzahl Columns
   	public int getColumnCount() {
   		return 6;
   	}
   
   	// Die Anzahl Rows
   	public int getRowCount() {
   		return 3;
   	}
   
   	// Die Titel der einzelnen Columns
   	public String getColumnName(int column) {
   		return String.valueOf( column );
   	}
   
   	// Der Wert der Zelle (rowIndex, columnIndex)
   	public Object getValueAt(int rowIndex, int columnIndex) {
   		return objects[ rowIndex ][ columnIndex ];
   	}

   	// Eine Angabe, welchen Typ von Objekten in den Columns angezeigt werden soll
   	public Class getColumnClass(int columnIndex) {
   		return Object.class;
   	}
}

Der neue TableCellRenderer entscheidet für jede Zelle einzeln, wie sie dargestellt werden soll.

class Renderer extends JLabel implements TableCellRenderer{
	private Color colorSelected = new Color( 200, 255, 200 );
	private Color colorFocus = new Color( 255, 200, 200 );
	private Color colorNormal = new Color( 200, 200, 255 );

	public Renderer(){
		setOpaque( true );
	}
	
	public Component getTableCellRendererComponent(JTable table, Object value,
			boolean isSelected, boolean hasFocus, int row, int column) {
		
		// die normalen Farben
		setForeground( Color.BLACK );
		if( hasFocus )
			setBackground( colorFocus );
		else if( isSelected )
			setBackground( colorSelected );
		else
			setBackground( colorNormal );
		
		setText( null );
		setIcon( null );
		
		if( value instanceof Date )
			setText( ((Date)value).toGMTString() );
		else if( value instanceof Icon )
			setIcon( (Icon)value );
		else if( value instanceof Color ){
			Color color = (Color)value;
			setForeground( color );
			setText( color.getRed() + ", " + color.getGreen() + ", " + color.getBlue() );
		}
		else if( value instanceof Boolean ){
			if( ((Boolean)value).booleanValue() )
				setText( "yes" );
			else
				setText( "no" );
		}
		else
			setText( value.toString() );
		
		return this;
	}
}

Datei:JTableTutorial16.png

Man beachte: in Column 3, ganz unten, ist ein String, und kein Date-Object. Da der Renderer für jede Zelle einzeln entscheiden kann, gibt es hier keine Probleme.

4. Darstellung auf Zeilen- oder Tabellen-Ebene

Normalerweise werden Renderer für eine bestimmte Datenklasse oder Datenspalte geschrieben.

Aber wie kann man Zeilen-abhängige oder Tabellen-abhängige Erfordnisse angehen? Z.B. wie kann man:

a) Zeilen abwechselnd in einer Tabelle färben b) Den Rand der selektierten Zelle ändern


Am einfachsten geht das indem man die Methode prepareRenderer() der Klasse JTable überschreibt:

import java.awt.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.table.*;
public class TableRowRendering extends JFrame{
    public TableRowRendering(){
        //  Model:
        Object[] columnNames = {"Type", "Date", "Company", "Shares", "Price"};
        Object[][] data =
        {
            {"Buy", new Date(), "IBM", new Integer(1000), new Double(80.50)},
            {"Sell",new Date(), "MicroSoft", new Integer(2000), new Double(6.25)},
            {"Sell",new Date(), "Apple", new Integer(3000), new Double(7.35)},
            {"Buy", new Date(), "Nortel", new Integer(4000), new Double(20.00)}
        };
        DefaultTableModel model = new DefaultTableModel(data, columnNames) {
            public Class getColumnClass(int column) {
                return getValueAt(0, column).getClass();
            }
        };
        // prepareRenderer überschreiben:
        table = new JTable( model ) {
            public Component prepareRenderer(
                    TableCellRenderer renderer, int row, int column) {
                Component c = super.prepareRenderer(renderer, row, column);
                if (!isRowSelected(row)) {
                    String type = (String)getModel().getValueAt(row, 0);
                    c.setBackground(row % 2 == 0 ? null : Color.LIGHT_GRAY );
                }
                if (isRowSelected(row) && isColumnSelected(column))
                    ((JComponent)c).setBorder(selected);
                return c;
            }
        };
        table.setPreferredScrollableViewportSize(table.getPreferredSize());
        getContentPane().add(new JScrollPane( table ), BorderLayout.CENTER);
    }
    private JTable table;
    private Border selected = new LineBorder(Color.GREEN);
    public static void main(String[] args) {
        TableRowRendering frame = new TableRowRendering();
        frame.setDefaultCloseOperation( EXIT_ON_CLOSE );
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible(true);
    }
}

5. Wichtige Bemerkung

Man sollte niemals einen Code schreiben, der so aussieht

public class Renderer implements TableCellRenderer {

	[...]

	public Component getTableCellRendererComponent(JTable table, Object value, 
		boolean isSelected, boolean hasFocus, int row, int column) {
      
		JLabel label = new JLabel();  // FALSCH

		[...]

		return label;
	}
}

Wieso nicht? Weil man immer wieder dieselbe Component verwenden kann. Denn das JTable ruft die getTableCellRendererComponent-Methode auf, und ruft gleich anschliessend den paint-Befehl der erhaltenen Component auf. Erst danach wird die "nächste" Component geholt.

Es ist wie das Tragen von Wasser: entweder läuft man mit einem Eimer zum Fluss, wieder zurück und wirft denn Eimer dann fort (weil er ja gebraucht ist), kauft sich dann einen neuen; oder man läuft mit demselben Eimer zurück zum Fluss. Ziemlich offensichtlich, dass die zweite Variante die bessere ist.

Verändern von Daten

Am Ende des 2. Teils wurde gezeigt, dass man auch Daten eingeben kann. Jetzt soll das nocheinmal vertieft behandelt werden.

Damit eine Zelle editiert werden kann, müssen die folgenden zwei Methoden im TableModel korrekt implementiert werden. Als Beispiel zeigen wir die Implementationen aus dem "DefaultTableModel":

   public boolean isCellEditable(int row, int column) {
        return true;
    }
   public void setValueAt(Object aValue, int row, int column) {
        Vector rowVector = (Vector)dataVector.elementAt(row);
        rowVector.setElementAt(aValue, column);
        fireTableCellUpdated(row, column);
    }

Bei dem Versuch, eine Zelle zu editieren, ruft das System zunächst die Methode "TableModel#isCellEditable" auf. Gibt sie "false" zurück, ist ein Editieren dieser Zelle nicht möglich. Gibt sie "true" zurück, kann der Wert der Zelle verändert werden. Wenn das geschehen ist, muss der neue Wert aber noch im TableModel abgespeichert werden. Dazu ruft das System automatisch die Methode "TableModel#setValueAt" auf und übergibt ihr den neuen Wert der Zelle. Die Methode "setValueAt" ist dann dafür verantwortlich, diesen Wert im TableModel abzuspeichern.


1. Der einfache Weg

Das JTable unterstützt von sich aus das Editieren von Zahlen und von Booleans. Alles andere wird wie ein String behandelt, d.h. da der Benutzer nur einen Text eingeben kann, wird dem Model ein String übergeben, welches es selbst in ein Objekt übersetzen muss.

Die Verwendung der Table-eigenen Editoren ist einfach:

Zuerst die Tabelle

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;

public class JTableDemo{
   public static void main( String[] args ){
      // Unser TableModel (siehe unten)
      Model model = new Model();
      
      // Das JTable initialisieren
      JTable table = new JTable( model );
            
      JFrame frame = new JFrame( "Demo" );
      frame.getContentPane().add( new JScrollPane( table ) );
      
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.pack();
      frame.setVisible( true );
   }
}

Jetzt das Model. Jede Zelle ist editierbar, doch jede Column wird anders behandelt.

class Model extends AbstractTableModel{
	private String[] strings;
	private int[] integers;
	private double[] doubles;
	private boolean[] booleans;
	
   	public Model(){
   		strings = new String[]{ "hallo", "ich", "bin", "eine", "Demo" };
   		integers = new int[]{ 1, 2, 3, 4, 5 };
   		doubles = new double[]{ 1.2, 3.4, 5.6, 7.8, 9.11 };
   		booleans = new boolean[]{ true, false, false, true, true };
   	}
   
   	// Die Anzahl Columns
   	public int getColumnCount() {
   		return 4;
   	}
   
   	// Die Anzahl Rows
   	public int getRowCount() {
   		return 5;
   	}
   
   	// Die Titel der einzelnen Columns
   	public String getColumnName(int column) {
   		switch( column ){
   			case 0: return "String";
   			case 1: return "Integer";
   			case 2: return "Double";
   			case 3: return "Boolean";
   			default: return null;
   		}
   	}
   
   	// Der Wert der Zelle (rowIndex, columnIndex)
   	public Object getValueAt(int rowIndex, int columnIndex) {
   		switch( columnIndex ){
   			case 0: return strings[ rowIndex ];
   			case 1: return new Integer( integers[ rowIndex ] );
   			case 2: return new Double( doubles[ rowIndex ] );
   			case 3: return new Boolean( booleans[ rowIndex ] );
   			default: return null;
   		}
   	}

   	// Eine Angabe, welchen Typ von Objekten in den Columns angezeigt werden soll
   	public Class getColumnClass(int columnIndex) {
   		switch( columnIndex ){
   			case 0: return String.class;
   			case 1: return Integer.class;
   			case 2: return Double.class;
   			case 3: return Boolean.class;
   			default: return null;
   		}
   	}
   	
   	// Jede Zelle ist editierbar
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return true;
	}
	
	// Wird aufgerufen, falls der Wert einer Zelle verändert wurde
	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
		switch( columnIndex ){
			case 0:
				strings[ rowIndex ] = aValue.toString();
				break;
			case 1:
				integers[ rowIndex ] = ((Integer)aValue).intValue();
				break;
			case 2:
				doubles[ rowIndex ] = ((Double)aValue).doubleValue();
				break;
			case 3:
				booleans[ rowIndex ] = ((Boolean)aValue).booleanValue();
		}
	}
}

JTableTutorial17.png

Es wichtig, dass man nicht vergisst, dass dies ausschliesslich für Strings (oder Objekte die als String dargestellt werden können, z.B. ein Datum), Nummern und Booleans so funktioniert!

2. Mit Hilfe eines TableCellEditors

Es ist möglich, eigene Editoren einzusetzen. Die Editoren werden vom JTable mit allen möglichen Informationen versorgt, welche sie irgendwie darstellen müssen. Die JTable wird dann warten, bis der Editor meldet, dass alle Daten eingegeben wurden.

Zuerst wird ein JTable aufgesetzt. Mit "setDefaultEditor" wird ein Editor mit einem Typ verbunden. Z.B. könnte der IntegerEditor mit "Integer.class" verbunden werden. Der Typ bezieht sich auf die Rückgabe von TableModel.getColumnClass.

import java.awt.Color;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;

import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;

public class JTableDemo{
    public static void main( String[] args ){
        // Einige Testdaten generieren
        DefaultTableModel model = new DefaultTableModel( new Object[]{ "A", "B" }, 0);
        model.addRow( new Object[]{ "1", "2" });
        model.addRow( new Object[]{ "3", "4" });
        model.addRow( new Object[]{ "5", "6" });
        model.addRow( new Object[]{ "7", "8" });
        JTable table = new JTable( model );
        // Den TestEditor einbinden. Er wird alle Spalten die als Typ
        // "Object" haben, bearbeiten.
        table.setDefaultEditor( Object.class, new TestEditor());
        
        // Die Tabelle anzeigen
        JFrame frame = new JFrame();
        frame.add( new JScrollPane( table ));
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible( true );
    }
}

Der Editor selbst ist eine ziemlich unabhängige Komponente. Oft ist es am einfachsten, direkt von einer Component zu erben, und zusätzlich das Interface TableCellEditor zu implementieren.

//Der Editor selbst ist ein einfaches JTextField
class TestEditor extends JTextField implements TableCellEditor, DocumentListener{
    private List<CellEditorListener> listeners = new ArrayList<CellEditorListener>();
    
    public TestEditor(){
        // Der Editor hört sich selbst ab, so kann er auf jede Benutzereingabe reagieren
        getDocument().addDocumentListener( this );
    }

    public Component getTableCellEditorComponent( JTable table, Object value, boolean isSelected, int row, int column ) {
        // Diese Methode wird von der JTable aufgerufen, wenn der Editor angezeigt werden soll
        setText( value.toString() );
        return this;
    }

    public void addCellEditorListener( CellEditorListener l ) {
        listeners.add( l );
    }

    public void cancelCellEditing() {
        // Falls abgebrochen wird, werden alle Listeners informiert
        ChangeEvent event = new ChangeEvent( this );
        for( CellEditorListener listener : listeners.toArray( new CellEditorListener[ listeners.size() ] ))
            listener.editingCanceled( event );
    }

    public Object getCellEditorValue() {
        // Gibt den aktuellen Wert des Editors zurück
        return getText();
    }

    public boolean isCellEditable( EventObject anEvent ) {
        // Im Falle eines MouseEvents, muss ein Doppelklick erfolgen, um den Editor zu aktivieren.
        // Ansonsten wird der Editor auf jeden Fall aktiviert
        if( anEvent instanceof MouseEvent )
            return ((MouseEvent)anEvent).getClickCount() > 1;
            
        return true;
    }

    public void removeCellEditorListener( CellEditorListener l ) {
        listeners.remove( l );
    }

    public boolean shouldSelectCell( EventObject anEvent ) {
        return true;
    }

    public boolean stopCellEditing() {
        // Sollte die Eingabe falsch sein, darf das editieren nich gestoppt werden
        if( !isValidText() )
            return false;
        
        // Ansonsten werden die Listener vom stop unterrichtet
        ChangeEvent event = new ChangeEvent( this );
        for( CellEditorListener listener : listeners.toArray( new CellEditorListener[ listeners.size() ] ))
            listener.editingStopped( event );
        
        return true;
    }

    public void changedUpdate( DocumentEvent e ) {
        update();
    }

    public void insertUpdate( DocumentEvent e ) {
        update();
    }

    public void removeUpdate( DocumentEvent e ) {
        update();
    }
    
    private boolean isValidText(){
        // Bestimmt, was eine gültige Eingabe ist.
        return getText().matches( "[1-9]+" );
    }
    
    public void update(){
        // Verändert die Umrandung des Editors, jenachdem, ob eine gültige
        // oder eine ungültige Eingabe gemacht wurde
        Color color;
        if( isValidText() )
            color = Color.GREEN;
        else
            color = Color.RED;
        
        setBorder( BorderFactory.createLineBorder( color ));
    }
}

3. TableCellEditor für eigene Typen

Schon in Teil 3 wurde gezeigt, dass das JTable Objekte des Types "Date" nicht richtig unterstützt. Das soll jetzt geändert werden.

Die Tabelle, man beachte, dass ein "DateRenderer" und ein "DateEditor" registriert werden.

import java.awt.Component;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.EventObject;
import java.util.Vector;
import java.util.List;
import java.util.ArrayList;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.SpinnerDateModel;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;

public class JTableDemo{
   public static void main( String[] args ){
      // Unser TableModel (siehe unten)
      Model model = new Model();
      
      // Das JTable initialisieren
      JTable table = new JTable( model );
      
      // Renderer und Editor für "Date" werden registriert
      table.setDefaultRenderer( Date.class, new DateRenderer() );
      table.setDefaultEditor( Date.class, new DateEditor() );
            
      JFrame frame = new JFrame( "Demo" );
      frame.getContentPane().add( new JScrollPane( table ) );
      
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.pack();
      frame.setVisible( true );
   }
}

Die Tabelle soll was sinnvolles darstellen: Notizen die aus einem Datum und aus einem Ereignis bestehen

// Eine einfache Klasse die Notizen beschreibt
class Note{
	private Date date;
	private String event;
	
	public Note( String event, Date date ){
		setEvent( event );
		setDate( date );
	}
	
	public String getEvent(){ return event; }
	public Date getDate(){ return date; }
	
	public void setEvent( String event ){ this.event = event; }
	public void setDate( Date date ){ this.date = date; }
}

Das TableModel, man beachte, dass es keine Verbindungen zum Renderer oder zum Editor besitzt! Allerdings kann durch das Wissen, dass "Date" nun unterstützt wird, in der Methode "setValueAt" ein Cast "Object -> Date" durchgeführt werden.

class Model extends AbstractTableModel{
	private Note[] notes;
	
   	public Model(){
   		notes = new Note[]{
   				new Note( "Mit Hund rausgehen", new Date(  104, 5, 2, 14, 0 ) ),
				new Note( "Kino", new Date( 104, 5, 7, 21, 30 ) ),
				new Note( "Abschlussprüfung", new Date( 104, 8, 14, 7, 0 ) ),
				new Note( "Einkaufen gehen", new Date( 104, 5, 2, 16, 0 ) ),
				new Note( "Rechnung bezahlen", new Date( 110, 11, 30, 23, 59 ) ),
   		};
   	}
   
   	// Die Anzahl Columns
   	public int getColumnCount() {
   		return 2;
   	}
   
   	// Die Anzahl Rows
   	public int getRowCount() {
   		return 5;
   	}
   
   	// Die Titel der einzelnen Columns
   	public String getColumnName(int column) {
   		if( column == 0 )
   			return "Ereignis";
   		else
   			return "Datum";
   	}
   
   	// Der Wert der Zelle (rowIndex, columnIndex)
   	public Object getValueAt(int rowIndex, int columnIndex) {
   		if( columnIndex == 0 )
   			return notes[ rowIndex ].getEvent();
   		else
   			return notes[ rowIndex ].getDate();
   	}

   	// Eine Angabe, welchen Typ von Objekten in den Columns angezeigt werden soll
   	public Class getColumnClass(int columnIndex) {
   		if( columnIndex == 0 )
   			return String.class;
   		else
   			return Date.class;
   	}
   	
   	// Jede Zelle ist editierbar
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return true;
	}
	
	// Wird aufgerufen, falls der Wert einer Zelle verändert wurde
	// Der Wert kommt entweder als String, oder von dem 
	// DateEditor (und daher als Date). 
	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
		if( columnIndex == 0 )
			notes[ rowIndex ].setEvent( (String)aValue );
		else
			notes[ rowIndex ].setDate( (Date)aValue );
	}
}

Der Renderer, der "Date" auf eine brauchbare Art darstellt ist einfach gehalten:

class DateRenderer extends DefaultTableCellRenderer{
	private DateFormat format; 
	
	public DateRenderer(){
		format = DateFormat.getDateTimeInstance();
	}
	
	public Component getTableCellRendererComponent(JTable table, Object value,
			boolean isSelected, boolean hasFocus, int row, int column) {

		// Date's werden auf einem einfache Weg in lesbare Texte umgewandelt.
		String text = format.format( (Date)value );
		
		return super.getTableCellRendererComponent(table, text, isSelected,
				hasFocus, row, column);
	}
}

Der Editor. Der Editor erbt von JSpinner. Glücklicherweise unterstützt der JSpinner "Date", und so kann man das meiste an den JSpinner delegieren

class DateEditor extends JSpinner implements TableCellEditor{
	private SpinnerDateModel model;
	
	private List listeners = new ArrayList();
	
	// Standardkonstruktor
	public DateEditor(){
		model = new SpinnerDateModel();
		model.setCalendarField( Calendar.DAY_OF_YEAR );
		setModel( model );
	}
	
	// Möglicherweise möchte jemand über Ereignisse des Editors
	// informiert werden
	public void addCellEditorListener(CellEditorListener l) {
		listeners.add( l );
	}
	
	// Ein CellEditorListener entfernen
	public void removeCellEditorListener(CellEditorListener l) {
		listeners.remove( l );
	}
	
	// Gibt den aktuellen Wert des Editors zurück.
	public Object getCellEditorValue() {
		return model.getDate();
	}
	
	// Gibt eine Component zurück, welche auf dem JTable dargestellt wird,
	// und mit der der Benutzer interagieren kann.
	public Component getTableCellEditorComponent(JTable table, Object value,
			boolean isSelected, int row, int column) {
		
		model.setValue( value );
		return this;
	}
	
	// Gibt an, ob die Zelle editierbar ist. Das EventObject kann
	// ein MouseEvent, ein KeyEvent oder sonst was sein.
	public boolean isCellEditable(EventObject anEvent) {
		return true;
	}
	
	// Gibt an, ob die Editor-Component selektiert werden muss, um
	// sie zu benutzen. Diese Editor soll immer selektiert werden,
	// deshalb wird hier true zurückgegeben
	public boolean shouldSelectCell(EventObject anEvent) {
		return true;
	}
	
	// Bricht das editieren der Zelle ab
	public void cancelCellEditing() {
		fireEditingCanceled();
	}
	
	// Stoppt das editieren der Zelle, sofern möglich.
	// Da der JSpinner immer einen gültigen Wert anzeigt, kann auch 
	// jederzeit gestoppt werden (return-Wert = true)
	public boolean stopCellEditing() {
		fireEditingStopped();
		return true;
	}
	
	// Benachrichtig alle Listener, dass das Editieren abgebrochen wurde
	protected void fireEditingCanceled(){
		ChangeEvent e = new ChangeEvent( this );
		for( int i = 0, n = listeners.size(); i<n; i++ )
			((CellEditorListener)listeners.get( i )).editingCanceled( e );
	}
	
	// Benachrichtig alle Listener, dass das Editieren beendet wurde
	protected void fireEditingStopped(){
		ChangeEvent e = new ChangeEvent( this );
		for( int i = 0, n = listeners.size(); i<n; i++ )
			((CellEditorListener)listeners.get( i )).editingStopped( e );
	}
}

JTableTutorial18.png

Also: der Editor ist eine Component die auf das JTable gesetzt wird, einen bestimmten Wert zugewiesen bekommt, und diesen Wert verändern darf. Klickt der Benutzer an eine andere Stelle, wird das Editieren entweder abgebrochen (cancelCellEditing) oder beendet (stopCellEditing), und der Wert dem TableModel über die Methode setValueAt überwiesen.

JTableHeader, TableColumn und das TableColumnModel

JTableHeader

Der JTableHeader ist ein Component, der die Darstellung der Tabellenspalten einer JTable übernimmt. Meistens braucht man diesen Component nicht, da JTable ihn standardmäßig mitinstantiiert. Die beiden Methoden getTableHeader() und setTableHeader() in JTable bieten an mit dem JTableHeader zu arbeiten. JTableHeader ist nicht dazu da um Spalten hinzuzufügen oder zu entfernen, sondern nur für die Darstellung zuständig. Für die Daten der Spalten ist das TableColumnModel zuständig, welches man wahlweise der JTable oder dem JTableHeader übergeben kann.

JTableHeader, in javax.swing.table, bietet einige interessante Methode an um die Spalten zu manipulieren. Die gebräuchlichsten beiden Methoden sind, finde ich, setReorderingAllowed(boolean reorderingAllowed) und setResizingAllowed(boolean resizingAllowed). Sie sollten selbsterklärend sein. Hier ein kleines Beispielprogramm:

import javax.swing.*;
import javax.swing.table.JTableHeader;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableColumn;



class TableHeaderTest {
	
	public static void main(String[] args) {
		// JTable initialisieren und mit Inhalt füllen; siehe dazu Teil 2
		JTable table = new JTable(new String[][]{{"1", "2", "3"},{"4", "5", "6"}}, new String[]{"A", "B", "C"});
		
		// JTableHeader holen
		JTableHeader header = table.getTableHeader();
		
		// ColumnModel holen:
		TableColumnModel columnModel = header.getColumnModel();
		
		// TableColumn erstellen
		TableColumn aColumn = new TableColumn();
		// setHeaderValue() setzt den Titel
		aColumn.setHeaderValue("D");
		// modelIndex zeigt an von welcher Spalte im DatenModel die neue Spalte ihre Werte holen soll
		aColumn.setModelIndex(1); // Index 1 im Model sind also "2" und "5"
		columnModel.addColumn(aColumn);
		
		// eine spalte verschieben
		columnModel.moveColumn(0, 2); // Spalte 0 (mit Titel "A") verschieben an Position 2 im Model
		
		// Spalten nicht resizable machen, nicht zulassen dass die Spaltenreihenfolge geändert werden kann
		header.setResizingAllowed(false);
		header.setReorderingAllowed(false);
		
		// JFrame konstruieren
		JFrame frame = new JFrame("JTableHeader test");
		// Table in JScrollPane einfügen und dem Frame hinzufügen
		frame.getContentPane().add(new JScrollPane(table));
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.pack();
		frame.setVisible(true);
	}
}

JTableTutorial19.png

Erklärung: Zuerst wird die Tabelle ganz normal angelegt, dann holt sich das Programm den JTableHeader der Tabelle. Vom TableHeader das ColumnModel, was aber auch direkt über die JTable gehen würde. Dann wird eine Spalte hinzugefügt, mit dem Model Index 1 (Das TableModel ist in Teil 2 erklärt). Danach wird die Spalte an position 0 ("A") vershcoben an position 2. Man muss immer daran denken dass das Zählen bei 0 anfängt, also sind mit den Zahlen immer die Positionen im Model gemeint. Letztendlich wird noch verboten die Spaltengröße, und die Spaltenreihenfolge zu ändern.

Header selber zeichnen

Eine weitere Möglichkeit von JTableHeader und TableColumn ist, wie auch bei JTable möglich, den Renderer für die Spalte zu setzen. Es wird das gleiche Renderer Interface benutzt wie bei der JTable: javax.swing.table.TableCellRenderer. Zugewiesen wird der Renderer mit setDefaultRenderer(TableCellRenderer defaultRenderer);

Beispiel:

import java.awt.Component;
import java.awt.Color;
import javax.swing.*;
import javax.swing.table.JTableHeader;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableCellRenderer;



class TableHeaderTest {
	
	public static void main(String[] args) {
		// JTable initialisieren und mit Inhalt füllen; siehe dazu Teil 2
		JTable table = new JTable(new String[][]{{"1", "2", "3"},{"4", "5", "6"}}, new String[]{"A", "B", "C"});
		
		// JTableHeader holen
		JTableHeader header = table.getTableHeader();
		
		// ColumnModel holen:
		TableColumnModel columnModel = header.getColumnModel();
		
		// TableColumn erstellen
		TableColumn aColumn = new TableColumn();
		// setHeaderValue() setzt den Titel
		aColumn.setHeaderValue("D");
		// modelIndex zeigt an von welcher Spalte im DatenModel die neue Spalte ihre Werte holen soll
		aColumn.setModelIndex(1); // Index 1 im Model sind also "2" und "5"
		columnModel.addColumn(aColumn);
		
		// eine spalte verschieben
		columnModel.moveColumn(0, 2); // spalte 0 (mit Tiel "A") verschieben an position 2 im model
		
		// spalten nicht resizable machen, nicht zulassen dass die spaltenreihenfolge geändert werden kann
		header.setResizingAllowed(false);
		header.setReorderingAllowed(false);
		
		// Renderer zuweisen
		header.setDefaultRenderer(new MyHeaderCellRenderer());
		
		// JFrame konstruieren
		JFrame frame = new JFrame("JTableHeader test");
		// Table in JScrollPane einfügen und dem Frame hinzufügen
		frame.getContentPane().add(new JScrollPane(table));
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.pack();
		frame.setVisible(true);
	}
}


// Eigener CellRenderer für die spaltenköpfe. Liefert immer ein 
// JLabel zurück, und implementiert TableCellRenderer
class MyHeaderCellRenderer extends JLabel implements TableCellRenderer {
	
	// einzig wichtige methode
	public Component getTableCellRendererComponent(JTable table, Object value, 
			boolean isSelected, boolean hasFocus, int row, int column) {
		// text
		setText(value.toString());
		// normale schriftart
		setFont(table.getFont());
		// der standard rahmen für spaltenköpfe
		setBorder(UIManager.getBorder("TableHeader.cellBorder"));
		// text zentiriert darstellen
		setHorizontalAlignment(SwingConstants.CENTER);
		// tooltip
		setToolTipText("Colum No. "+(column+1));
		// undurchsichtig damit man die hintergrundfarbe sieht.
		setOpaque(true);
		// je nach spalte die hintergrundfarbe setzen
		switch(column) {
			case 0:  setBackground(Color.GREEN); break;
			case 1:  setBackground(Color.BLUE); break;
			case 2:  setBackground(Color.YELLOW); break;
			case 3:  setBackground(Color.RED); break;
			default: setBackground(Color.LIGHT_GRAY);
		}
		return this;
	}
}

JTableTutorial20.png

Hier wird zwar nur die Hintergrundfarbe geändert, Aber es gibt noch sehr viel mehr Möglichkeiten Die Spaltenköpfe zu manipulieren mithilfe des CellRenderers. Genau wie in Teil 3 beschrieben kann man auch andere komponenten die Darstellung der Header übernehmen lassen. Dazu muss man allerdings den CellRenderer anpassen (siehe auch Teil:

class MyHeaderCellRenderer implements TableCellRenderer {
	
	// einzig wichtige methode
	public Component getTableCellRendererComponent(JTable table, Object value, 
			boolean isSelected, boolean hasFocus, int row, int column) {
		JComponent c = null;

	        if(value instanceof JCheckBox) {
			c = (JCheckBox) value;
		} else if(value instanceof JComboBox) {
			c = (JComboBox) value;
		} else {
			c = new JLabel(value.toString());
			((JLabel)c).setHorizontalAlignment(SwingConstants.CENTER);
		}
		c.setEnabled(true);
		// normale schriftart
		c.setFont(table.getFont());
		// der standard rahmen für spaltenköpfe
		c.setBorder(UIManager.getBorder("TableHeader.cellBorder"));
		// text zentriert darstellen
		c.setToolTipText("Colum No. "+(column+1));
		// undurchsichtig damit man die Hintergrundfarbe sieht.
		c.setOpaque(true);
		// je nach spalte die hintergrundfarbe setzen
		switch(column) {
			case 0:  c.setBackground(Color.GREEN); break;
			case 1:  c.setBackground(Color.BLUE); break;
			case 2:  c.setBackground(Color.YELLOW); break;
			case 3:  c.setBackground(Color.RED); break;
			default: c.setBackground(Color.LIGHT_GRAY);
		}
		return c;
	}

Jetzt gibt es aber das Problem dass man die Komponenten nicht ändern kann. Das ist auch etwas komplizierter, aber gute Tutorials, die sich damit eingehender beschäftigen, kann man im Netz finden. Dort sind auch andere mehrere tiefergehende Beispiele zu JTableHeader und TableColumn.

Ein Header für die Rows

Standartmässig unterstützt das JTable nur einen Header, der über den Spalten steht.

Es ist aber kein grosses Problem eine Component - TableRowHeader - zu erstellen, welche dem JScrollPane übergeben werden kann auf dem das JTable sitzt. Dazu wird die Methode JScrollPane#setRowHeader benutzt.

Da man es sich nicht leisten kann, bei jedem Aufruf von paint den gesammten Header neu darzustellen (sobald die Tabelle viele Einträge kriegt (1000 oder mehr), würde enorm viel Zeit in das Zeichnen investiert werden), zeichnet die der Header nur gerade die "Knöpfe", welche auch sichtbar sind.

Da die Spalten einer JTable nicht immer gleichhoch sein müssen, muss der Header jede Spalte einzeln beachten, und nicht einfach eine Rechnung machen, um herauszufinden wo gezeichnet, und wo nicht gezeichnet werden muss.

Das Demoframe. Man beachte, dass der Header dem ScrollPane übergeben werden muss.

public class JTableDemo {
	public static void main( String[] args ) {
		// Das JTable initialisieren
		Vector title = new Vector();
		title.add( "A" );
		title.add( "B" );
		title.add( "C" );
		title.add( "D" );
		
		JTable table = new JTable( createData(), title );

		// Jede 10. Reihe wird vergrössert, um zu demonstrieren, wie sich der TableRowHeader anpasst.
		int rows = table.getRowCount();
		for( int i = 0; i < rows; i += 10 )
			table.setRowHeight( i, 2*table.getRowHeight( i ) );
		
		JFrame frame = new JFrame( "Demo" );
		JScrollPane pane = new JScrollPane( table );
		
		// mit diesem Befehl wird die Leiste links angesetzt.
		pane.setRowHeader( new TableRowHeader( table, pane ) );
		
		frame.getContentPane().add( pane );
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.pack();
		frame.setVisible( true );
	}
	
	// Produziert genug Daten, dass man scrollen kann.
	private static Vector createData(){
		Vector data = new Vector();
		
		for( int i = 0; i < 10000; i++ ){
			Vector temp = new Vector();
			for( int j = 0; j < 4; j++ )
				temp.add( String.valueOf( Math.random() ) );
			
			data.add( temp );
		}
		
		return data;
	}
}

Der TableRowHeader. Eigentlich nichts weiter als eine ganz normale Component.

public class TableRowHeader extends JViewport implements AdjustmentListener{
	private JTable	table;
	private Cell cell;

	public TableRowHeader( JTable table, JScrollPane parent ) {
		this.table = table;
		cell = new Cell();
		setView( new Dummy() );
		setPreferredSize( new Dimension( 100, 0 ) );
		
		parent.getVerticalScrollBar().addAdjustmentListener( this );
		parent.getHorizontalScrollBar().addAdjustmentListener( this );
	}
	
	public void adjustmentValueChanged( AdjustmentEvent e ) {
		repaint();
	}

	protected void paintComponent(Graphics g){
		super.paintComponent( g );
		Rectangle rec = TableRowHeader.this.getViewRect();

		int y = 0;
		int rows = table.getRowCount();
		int index = 0;

		if( rows == 0 )
			return;

		if( y + table.getRowHeight( 0 ) < rec.y ){
			while( index < rows ){
				int height = table.getRowHeight( index );

				if( y + height < rec.y ){
					y += height;
					index++;
				}
				else
					break;
			}
		}

		int max = rec.y + rec.height;
		int width = getWidth();

		while( y < max && index < rows ){
			int height = table.getRowHeight( index );
			cell.set( index );
			SwingUtilities.paintComponent( g, cell, this, 0, y-rec.y, width, height );
            
			y += height;
			index++;
		}
	}

	// Nur eine leere Hülle, damit eine "Simulation" aufgebaut werden kann.
	private static class Dummy extends JComponent{
		public void paint( Graphics g ){
			// do nothing
		}

		public void update( Graphics g ) {
			// do nothing
		}
	}
	
	// Stellt den Knopf für eine Reihe dar.
	private class Cell extends JLabel {
		public void set( int row ) {
			setHorizontalAlignment( CENTER );
			setForeground( TableRowHeader.this.getForeground() );
			setBackground( TableRowHeader.this.getBackground() );
			setFont( TableRowHeader.this.getFont() );
			setText( "Row " + row );
			setBorder( UIManager.getBorder( "TableHeader.cellBorder" ) );
		}
	}
}

Und daraus ergibt sich schliesslich dieses Bild:

JTableTutorial21.png

Selbstverständlich könnte man den Header ausbauen. So wäre vielleicht ein verschieben von Zeilen möglich... Ein Beispiel das die Möglichkeiten zeigt gibt es ja: den JTableHeader.

Weitere Links


Fragen

Das Thema wurde nicht ausreichend behandelt? Du hast Fragen dazu und brauchst weitere Informationen?

Besuche uns im Byte-Welt-Forum, wir helfen Dir gerne!


Dir hat dieser Artikel gefallen? Schreibe einen Kommentar

Du musst angemeldet sein, um einen Kommentar abzugeben.


-- Beni (© Benjamin Sigg, 04.06.2004, 20:30)