Wie kann man Tabellenspalten nach eigenen Kriterien sortieren?
Das Beispiel
Das Beispiel zeigt eine einfache, erdachte Tabelle mit
Einträgen von Bezeichnungen, Seriennummern und Preisen von
Haushaltsgegenständen. Es demonstriert, wie bei den drei
Spalten unterschiedliche Sortierungen eingerichtet werden
können, sodass durch Klick auf den Spaltenkopf der Inhalt der
jeweiligen Spalte wechselweise auf- und absteigend sortiert wird.
Darüber
hinaus wird gezeigt, wie die Spalten eines Tabellenkopfs mit
verschiedenen Tooltips versehen werden können.
Das GUI
Die Klasse SpalteSortieren
ist von einem JFrame
abgeleitet und liefert die grafische Oberfläche des Programms.
Das Fenster zeigt eine einfache JTable
, die mit drei
Spalten und fünf Reihen gefüllt ist, in denen
Haushaltsgegenstände mit einer Seriennummer und ihrem Preis
aufgelistet sind.
Die Oberfläche wird durch die Methode initGUI()
erzeugt, die im Konstruktor aufgerufen wird.
private void initGUI() { DefaultTableModel model = createModel(); JTable table = new JTable(model); table.setShowGrid(true); table.setGridColor(Color.GRAY); JTableHeader header = createTableHeader(table.getColumnModel()); header.setBackground(Color.LIGHT_GRAY); table.setTableHeader(header); table.setAutoCreateRowSorter(true); TableRowSorter<DefaultTableModel> newSorter = new TableRowSorter<DefaultTableModel>( model); header.addMouseListener(new MouseAdapter() { public void mouseReleased(MouseEvent e) { sortColumns(e, table, newSorter); } }); this.add(new JScrollPane(table), BorderLayout.CENTER); } //... private DefaultTableModel createModel() { String[] header = { "Objekt", "Seriennummer", "Preis" }; String[][] data = { new String[] { "B\u00FCgeleisen", "kg-04-7385", "68,98" }, new String[] { "Staubsauger", "24.17-123456", "417" }, new String[] { "Eierkocher", "eikoch-9413", "19,95" }, new String[] { "Luftpumpe", "0034-lp-1713", "19,95" }, new String[] { "Schreibtisch", "x24-scht-0815", "235,50" } }; return new DefaultTableModel(data, header); }
Hier wird zunächst ein DefaultTableModel
durch die
Methode createModel()
erzeugt, das die Inhalte des
Tabellen-Views verwaltet. Es umfasst ein einfaches String
-Array
für den Tabellenkopf und ein zweidimensionales String
-Array
des Tabelleninhaltes.
Die Tabelle wird mit dem Model
initialisiert und mit einem Raster und der Linienfarbe Grau
versehen. Letzteres geschieht, da die Standard-Linienfarbe Weiß
ist, sodass das Raster bei weißem Hintergrund nicht zu sehen
wäre.
Es folgt die Einrichtung des Tabellenkopfes, der durch die Methode createTableHeader()
zurückgegeben und durch JTable#setTableHeader()
der Tabelle hinzugefügt wird. Das Verfahren ist notwendig, um
dem Tabellenkopf die Tooltips hinzuzufügen.
Tooltips auf dem Tabellenkopf
In createTableHeader()
wird ein Objekt vom Typ JTableHeader
durch Übergabe des TableColumnModel
der Tabelle
erzeugt. In der anonymen Klasse wird dort die Methode JTableHeader#getToolTipText(MouseEvent
e)
überschrieben.
private JTableHeader createTableHeader(TableColumnModel cm) { return new JTableHeader(cm) { public String getToolTipText(MouseEvent e) { Point p = e.getPoint(); int ci = cm.getColumnIndexAtX(p.x); int mi = cm.getColumn(ci).getModelIndex(); return createToolTips()[mi]; } }; }
Um den Spaltenkopf zu registrieren, wird durch das übergebene MouseEvent
die Position des Cursors über ein Point
-Objekt
bestimmt und aus dem ColumnModel dann der Index der zugehörigen
Spalte ermittelt.
Die Tooltips selbst werden in einer gesonderten
Methode, createToolTips()
, als String-Array erzeugt und
über den Spaltenindex aufgerufen. Nebenbei wird hier beim
Tooltip für die mittlere Spalte demonstriert, dass Tooltips in
Java auch mit HTML formatiert werden können.
private String[] createToolTips() { return new String[] { "Click sortiert lexikalisch", "<html>Click sortiert lexikalisch,<br>Alt-Click sortiert nach der Endnummer</html>", "Click sortiert numerisch" }; }
Die Sortierung durch RowSorter
Die Methode JTable#setAutoCreateRowSorter(true)
ermöglicht durch die Übergabe des Parameters true
ein automatisches Sortieren der Tabelle dergestalt, dass durch Klick
auf den Kopf einer Spalte diese lexikalisch sortiert wird.
Wiederholtes Klicken führt zur gegenläufigen Sortierung.
Intern wird hierdurch der Tabelle ein Objekt vom Typ TableRowSorter
hinzugefügt. Die Belegung des Inhalts der restlichen Tabelle
wird bei der Sortierung entsprechend angepasst, sodass die
Konsistenz der Tabellenreihen gewahrt wird.
Die Sortierung erfolgt durch abstrakte RowSorter
, die
durch die Klasse TableRowSorter
konkretisiert werden.
Ein Objekt vom Typ TableRowSorter<DefaultTableModel>()
wird erstellt und diesem das aktuelle TableModel als Gegenstand der
Sortierung übergeben. Zum Schluss wird der Tabellenkopf bei
einem MouseListener
angemeldet, die Tabelle einem JScrollPane
hinzugefügt und dies mittig auf das Programmfenster gesetzt.
private void sortColumns(MouseEvent e, JTable table, TableRowSorter<DefaultTableModel> newSorter) { int spalte = table.columnAtPoint(e.getPoint()); String spaltenName = table.getColumnName(spalte); table.setRowSorter(newSorter); if (spaltenName.equals("Seriennummer") && e.isAltDown()) { newSorter.setComparator(spalte, new NumberSorter("-")); newSorter.sort(); } else if (spaltenName.equals("Preis")) { newSorter.setComparator(spalte, new DecimalSorter()); newSorter.sort(); } }
Der erwähnte MouseListener
des Tabellenkopfes ist
im Beispiel durch einen MouseAdapter
realisiert, da
hier lediglich die Reaktion auf mouseReleased()
benötigt wird. Hier wird die Methode sortColumns()
aufgerufen, die die gesamte Routine für die Sortierung
enthält. Ihr werden hierzu das MouseEvent
, das
Tabellen-Objekt und der neu gebildete TableRowSorter
übergeben.
Die Methode ermittelt zunächst den Spaltenindex und den
Spaltennamen über das MouseEvent
. Nachdem der neu
gebildete TableRowSorter
auf die Tabelle gesetzt wurde,
wird mittels einer Verzweigung anhand der Spaltennamen zwischen den
Spalten unterschieden und den Sortern Objekte verschiedener Comparator
-Klassen
zugewiesen. Sie dienen dem konkreten Vergleich der
Einzeleinträge und ermöglichen so erst die Sortierung.
Für die Spalten "Seriennummer" und "Preis" werden hier
gesonderte, unten erläuterte Klassen verwendet. Für die
verbleibende Spalte "Objekt" wird die lexikalische
Standard-Sortierung beibehalten.
Um die numerische Sortierung der
Spalte "Seriennummer" zu aktivieren, muss zusätzlich die Alt-Taste
gedrückt werden, ansonsten findet auch hier eine lexikalische
Sortierung statt. Es ist wichtig, nach der Zuweisung jeweils die
Methode DefaultRowSorter#sort()
aufzurufen, um die
Sortierung anzustoßen.
Die Comparator-Klassen
Comparator
-Klassen dienen dem Vergleich zweier Werte.
Sie implementieren das Interface Comparator
und
müssen somit die Methode compare()
konkretisieren,
in der der Vergleichsalgorithmus implementiert wird.
NumberSorter
Die Klasse dient dazu, Strings, die mit einer Zahl enden, in der Reihenfolge dieser Endungen zu sortieren. Im Beispiel besitzt jedes Objekt in der mittleren Spalte eine Seriennummer, die, getrennt durch ein '-', am Ende jeweils eine Nummer mit unterschiedlicher Stellenzahl besitzt. Eine lexikalische Sortierung kommt daher nicht in Frage, wenn die Zahlenwerte der Größe nach erfasst werden sollen. Der Zahlenanteil des Strings muss also zunächst separiert, dann als Zahl interpretiert und schließlich sortiert werden.
class NumberSorter implements Comparator<String> { final String trenner; public NumberSorter(final String trenner) { this.trenner = trenner; } @Override public int compare(String s1, String s2) { int pos1 = s1.lastIndexOf(trenner) + 1; int pos2 = s2.lastIndexOf(trenner) + 1; if (pos1 > -1) { s1 = ((String) s1).substring(pos1); } if (pos2 > -1) { s2 = ((String) s2).substring(pos2); } if (s1 instanceof String && ((String) s1).length() == 0) { s1 = null; } if (s2 instanceof String && ((String) s2).length() == 0) { s2 = null; } if (s1 == null && s2 == null) { return 0; } else if (s1 == null) { return 1; } else if (s2 == null) { return -1; } else { try { int i1 = new Integer(s1).intValue(); int i2 = new Integer(s2).intValue(); int erg = i1 < i2 ? 1 : i1 > i2 ? -1 : 0; return erg; } catch (NumberFormatException e) { } } return ((String) s1).compareTo((String) s2); } }
Das Trennzeichen wird dem Konstruktor der Comparator
-Klasse
übergeben, da die Signatur von compare()
selbst
nicht geändert werden kann. In der Methode werden die
numerischen Anteile zunächst durch String#substring()
ermittelt und dann, nach einigen Sicherheitsprüfungen, in Integer
gewandelt. Diese können in primitive int
-Werte
unboxed und anschließend verglichen werden. Durch den Vergleich
müssen ganzzahlige Werte größer 0, kleiner 0 oder bei
Gleichheit gleich 0 zurückgegeben werden.
DecimalSorter
Die Klasse DecimalSorter
dient dazu, die in der Spalte
"Preis" angegebenen Zahlen zu sortieren, da auch hier eine
lexikalische Sortierung nicht zum gewünschten Ergebnis
führen würde.
Auch hier findet eine Wandlung der zur
Sortierung anstehenden Strings in numerische Werte statt. Allerdings
sind die Dezimalzahlen mit einem Komma als Dezimaltrenner versehen
und müssen in double
gewandelt werden. Die
Sortierung durch Vergleich erfolgt dann auf eine ähnliche Weise
wie oben.
class DecimalSorter implements Comparator<String> { @Override public int compare(String o1, String o2) { Double d1 = convertToDouble(o1); Double d2 = convertToDouble(o2); if (d1 == null && d2 == null) { return 0; } else if (d1 == null) { return 1; } else if (d2 == null) { return -1; } return d1 < d2 ? 1 : d2 < d1 ? -1 : 0; } private Double convertToDouble(String s) { if (s.indexOf(",") > -1) s = s.replace(",", "."); if (s.matches("-?\\d+([.]{1}\\d+)?")) { try { return new Double(s); } catch (NumberFormatException e) { } } return null; } }
Zuvor jedoch müssen die Spaltenwerte in der Methode convertToDouble()
konvertiert werden. Dies geschieht in zwei Schritten: Zunächst
werden die Kommata durch Punkte ersetzt. Es findet dann eine
Prüfung über einen regulären Ausdruck statt, der
sicherstellt, dass es sich bei dem geprüften String um einen
ganzzahligen oder dezimalen Zahlenwert handelt. Anschließend
finden die Konvertierung zu Double
und dessen
Rückgabe statt.
Der eigentliche Vergleich findet in compare()
schließlich wieder über einen Größenvergleich
statt.
Vollständiger Quelltext
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Comparator;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableRowSorter;
public class SpalteSortieren extends JFrame {
public SpalteSortieren() {
initGUI();
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("Tabelle nach Spalten sortieren");
this.setSize(600, 400);
this.setLocationRelativeTo(null);
this.setVisible(true);
}
private void initGUI() {
DefaultTableModel model = createModel();
JTable table = new JTable(model);
table.setShowGrid(true);
table.setGridColor(Color.GRAY);
JTableHeader header = createTableHeader(table.getColumnModel());
header.setBackground(Color.LIGHT_GRAY);
table.setTableHeader(header);
table.setAutoCreateRowSorter(true);
TableRowSorter<DefaultTableModel> newSorter = new TableRowSorter<DefaultTableModel>(
model);
header.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
sortColumns(e, table, newSorter);
}
});
this.add(new JScrollPane(table), BorderLayout.CENTER);
}
private void sortColumns(MouseEvent e, JTable table, TableRowSorter<DefaultTableModel> newSorter) {
int spalte = table.columnAtPoint(e.getPoint());
String spaltenName = table.getColumnName(spalte);
table.setRowSorter(newSorter);
if (spaltenName.equals("Seriennummer") && e.isAltDown()) {
newSorter.setComparator(spalte, new NumberSorter("-"));
newSorter.sort();
} else if (spaltenName.equals("Preis")) {
newSorter.setComparator(spalte, new DecimalSorter());
newSorter.sort();
}
}
private JTableHeader createTableHeader(TableColumnModel cm) {
return new JTableHeader(cm) {
public String getToolTipText(MouseEvent e) {
Point p = e.getPoint();
int ci = cm.getColumnIndexAtX(p.x);
int mi = cm.getColumn(ci).getModelIndex();
return createToolTips()[mi];
}
};
}
private String[] createToolTips() {
return new String[] {
"Click sortiert lexikalisch",
"<html>Click sortiert lexikalisch,<br>Alt-Click sortiert nach der Endnummer",
"Click sortiert numerisch" };
}
private DefaultTableModel createModel() {
String[] header = { "Objekt", "Seriennummer", "Preis" };
String[][] data = {
new String[] { "B\u00FCgeleisen", "kg-04-7385", "68,98" },
new String[] { "Staubsauger", "24.17-123456", "417" },
new String[] { "Eierkocher", "eikoch-9413", "19,95" },
new String[] { "Luftpumpe", "0034-lp-1713", "19,95" },
new String[] { "Schreibtisch", "x24-scht-0815", "235,50" } };
return new DefaultTableModel(data, header);
}
public static void main(String[] args) {
new SpalteSortieren();
}
}
class NumberSorter implements Comparator<String> {
final String trenner;
public NumberSorter(final String trenner) {
this.trenner = trenner;
}
@Override
public int compare(String s1, String s2) {
int pos1 = s1.lastIndexOf(trenner) + 1;
int pos2 = s2.lastIndexOf(trenner) + 1;
if (pos1 > -1) {
s1 = ((String) s1).substring(pos1);
}
if (pos2 > -1) {
s2 = ((String) s2).substring(pos2);
}
if (s1 instanceof String && ((String) s1).length() == 0) {
s1 = null;
}
if (s2 instanceof String && ((String) s2).length() == 0) {
s2 = null;
}
if (s1 == null && s2 == null) {
return 0;
} else if (s1 == null) {
return 1;
} else if (s2 == null) {
return -1;
} else {
try {
int i1 = new Integer(s1).intValue();
int i2 = new Integer(s2).intValue();
int erg = i1 < i2 ? 1 : i1 > i2 ? -1 : 0;
return erg;
} catch (NumberFormatException e) {
}
}
return ((String) s1).compareTo((String) s2);
}
}
class DecimalSorter implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
Double d1 = convertToDouble(o1);
Double d2 = convertToDouble(o2);
if (d1 == null && d2 == null) {
return 0;
} else if (d1 == null) {
return 1;
} else if (d2 == null) {
return -1;
}
return d1 < d2 ? 1 : d2 < d1 ? -1 : 0;
}
private Double convertToDouble(String s) {
if (s.indexOf(",") > -1)
s = s.replace(",", ".");
if (s.matches("-?\\d+([.]{1}\\d+)?")) {
try {
return new Double(s);
} catch (NumberFormatException e) {
}
}
return null;
}
}
Quellen
http://docs.oracle.com/javase/tutorial/uiswing/components/table.html#headertooltip