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
erzeugt 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.SwingUtilities;
import javax.swing.table.TableRowSorter;
public class SpalteSortieren {
public SpalteSortieren() {
initGUI();
}
private void initGUI() {
JFrame frame = new JFrame("Tabelle nach Spalten sortieren");
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);
}
});
frame.add(new JScrollPane(table), BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
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) {
SwingUtilities.invokeLater(() -> 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
https://docs.oracle.com/javase/tutorial/uiswing/components/table.html#headertooltip
Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.