Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 38 - Swing: Komponenten II |
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 Arrayelemente 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()); 015 JTable table = new JTable(DATA, COLHEADS); 016 Container cp = getContentPane(); 017 cp.add(new JLabel("Alte c\'t-Ausgaben:"), "North"); 018 cp.add(new JScrollPane(table), "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 |
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. |
|
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 |
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.
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
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.
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. |
|
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. |
|
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. |
|
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. |
|
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.
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 |
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()); 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), "Center"); 025 JButton button = new JButton("Drucken"); 026 button.addActionListener(this); 027 cp.add(button, "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 |
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-
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()); 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:"), "North"); 041 cp.add(new JScrollPane(table), "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 |
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
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 |
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()); 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:"), "North"); 021 cp.add(new JScrollPane(table), "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 |
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. |
|
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 | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |