Saturday, June 30, 2007

Sorting and Filtering Tables

This Tech Tip reprinted with permission by java.sun.com

Java Standard Edition (Java SE) 6.0 (code name Mustang), adds some features that make sorting and filtering the contents of a Swing JTable much easier. (Final inclusion of these features is subject to JCP approval.) Most modern table-driven user interfaces allow users to sort columns by clicking on the table header. This could be done with the Swing JTable support in place prior to Mustang. However the functionality had to be added manually in a custom way for each table that needed this feature. With Mustang, enabling this functionality requires minimal effort. Filtering is another option commonly available with user interfaces. Filtering allows users to display only the rows in a table that match user-supplied criteria. With Mustang, enabling filtering of JTable contents is also much easier.
Sorting Rows

The basis for sorting and filtering rows in Mustang is the abstract RowSorter class. RowSorter maintains two mappings, one of rows in a JTable to the elements of the underlying model, and back again. This allows for one to do sorting and filtering. The class is generic enough to work with both TableModel and ListModel. However only a TableRowSorter is provided with the Mustang libraries to work with JTable.

In the simplest case, you pass the TableModel to the TableRowSorter constructor, and then pass the created RowSorter into the setRowSorter() method of JTable. Here's an example program, SortTable, that demonstrates the approach:
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;

public class SortTable {
public static void main(String args[]) {
Runnable runner = new Runnable() {
public void run() {
JFrame frame = new JFrame("Sorting JTable");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Object rows[][] = {
{"AMZN", "Amazon", 41.28},
{"EBAY", "eBay", 41.57},
{"GOOG", "Google", 388.33},
{"MSFT", "Microsoft", 26.56},
{"NOK", "Nokia Corp", 17.13},
{"ORCL", "Oracle Corp.", 12.52},
{"SUNW", "Sun Microsystems", 3.86},
{"TWX", "Time Warner", 17.66},
{"VOD", "Vodafone Group", 26.02},
{"YHOO", "Yahoo!", 37.69}
};
String columns[] = {"Symbol", "Name", "Price"};
TableModel model =
new DefaultTableModel(rows, columns) {
public Class getColumnClass(int column) {
Class returnValue;
if ((column >= 0) && (column < getColumnCount())) {
returnValue = getValueAt(0, column).getClass();
} else {
returnValue = Object.class;
}
return returnValue;
}
};

JTable table = new JTable(model);
RowSorter sorter =
new TableRowSorter(model);
table.setRowSorter(sorter);
JScrollPane pane = new JScrollPane(table);
frame.add(pane, BorderLayout.CENTER);
frame.setSize(300, 150);
frame.setVisible(true);
}
};
EventQueue.invokeLater(runner);
}
}
Image

Click on one of the columns of the displayed table, and notice that the contents of the column are reordered.
Image

You might ask why not simply use the DefaultTableModel, as opposed to creating a custom subclass of it? The answer is that TableRowSorter has a set of rules to follow for sorting columns. By default, all columns of a table are thought to be of type Object. So, sorting is done by calling toString(). By overriding the default getColumnClass() behavior of DefaultTableModel, RowSorter sorts according to the rules of that class, assuming it implements Comparable. You can also install a custom Comparator for a column by calling setComparator(int column, Comparator comparator).

The key three lines in the SortTable program that are pertinent to sorting are shown here:
JTable table = new JTable(model);
RowSorter sorter =
new TableRowSorter(model);
table.setRowSorter(sorter);

The first line associates the model with the table. The second line creates a RowSorter specific to the model. The third line associates the RowSorter with the JTable. This enables a user to click on the column header to sort that column. Clicking a second time on the same column reverses the sort order.

If you want to add your own action when the sort order changes, you can attach a RowSorterListener to the RowSorter. The interface has one method:

void sorterChanged(RowSorterEvent e)

The method allows you to update the text on the status bar, or perform some additional task. The RowSorterEvent for the action allows you to discover how many rows were present before the sort, in the event that the RowSorter filtered rows in or out of the view.
Filtering Table Rows

You can associate a RowFilter with the TableRowSorter and use it to filter the contents of a table. For instance, you can use a RowFilter such that a table displays only rows where the name starts with the letter A or where the stock price is greater than $50. The abstract RowFilter class has one method that is used for filtering:

boolean include(RowFilter.Entry entry)

For each entry in the model associated with the RowSorter, the method indicates whether the specified entry should be shown in the current view of the model. In many cases, you don't need to create your own RowFilter implementation. Instead, RowFilter offers six static methods for creating filters.

* andFilter(Iterable> filters)
* dateFilter(RowFilter.ComparisonType type, Date date, int... indices)
* notFilter(RowFilter filter)
* numberFilter(RowFilter.ComparisonType type, Number number, int... indices)
* orFilter(Iterable> filters)
* regexFilter(String regex, int... indices)

For the RowFilter factory methods that have an argument of indices (dateFilter, numberFilter, regexFilter), only the set of columns corresponding to the specified indices are checked in the model. If no indices are specified, all columns are checked for a match.

The dateFilter allows you to check for a matching date. The numberFilter checks for a matching number. The notFilter is used for reversing another filter, in other words, it includes entries that the supplied filter does not include. You can use it to do things like find entries where something was not done on 12/25/2005. The andFilter and orFilter are for logically combining other filters. The regexFilter uses a regular expression for filtering. Here's a program, FilterTable, that uses a regexFilter to filter table content:
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.util.regex.*;

public class FilterTable {
public static void main(String args[]) {
Runnable runner = new Runnable() {
public void run() {
JFrame frame = new JFrame("Sorting JTable");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Object rows[][] = {
{"AMZN", "Amazon", 41.28},
{"EBAY", "eBay", 41.57},
{"GOOG", "Google", 388.33},
{"MSFT", "Microsoft", 26.56},
{"NOK", "Nokia Corp", 17.13},
{"ORCL", "Oracle Corp.", 12.52},
{"SUNW", "Sun Microsystems", 3.86},
{"TWX", "Time Warner", 17.66},
{"VOD", "Vodafone Group", 26.02},
{"YHOO", "Yahoo!", 37.69}
};
Object columns[] = {"Symbol", "Name", "Price"};
TableModel model =
new DefaultTableModel(rows, columns) {
public Class getColumnClass(int column) {
Class returnValue;
if ((column >= 0) && (column < getColumnCount())) {
returnValue = getValueAt(0, column).getClass();
} else {
returnValue = Object.class;
}
return returnValue;
}
};
JTable table = new JTable(model);
final TableRowSorter sorter =
new TableRowSorter(model);
table.setRowSorter(sorter);
JScrollPane pane = new JScrollPane(table);
frame.add(pane, BorderLayout.CENTER);
JPanel panel = new JPanel(new BorderLayout());
JLabel label = new JLabel("Filter");
panel.add(label, BorderLayout.WEST);
final JTextField filterText =
new JTextField("SUN");
panel.add(filterText, BorderLayout.CENTER);
frame.add(panel, BorderLayout.NORTH);
JButton button = new JButton("Filter");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String text = filterText.getText();
if (text.length() == 0) {
sorter.setRowFilter(null);
} else {
try {
sorter.setRowFilter(
RowFilter.regexFilter(text));
} catch (PatternSyntaxException pse) {
System.err.println("Bad regex pattern");
}
}
}
});
frame.add(button, BorderLayout.SOUTH);
frame.setSize(300, 250);
frame.setVisible(true);
}
};
EventQueue.invokeLater(runner);
}
}

The display sets a filter for all strings with the characters SUN somewhere in them. This is specified by the string "SUN". Use the characters '^' and '$' to test for exact matches at the beginning and end of the string respectively.
Image

The filter internally uses Matcher.find() for inclusion testing when the user presses the "Filter" button at the bottom.
Image

Change the filter text to change the set of rows shown in the table. If you want to see all the rows in the table, remove the filter text.

One last thing worth mentioning -- when sorting or filtering, the selection is in terms of the view. So that if you need to map to the underlying model, you need to call the convertRowIndexToModel() method. Similarly, if you want to convert from the model to the view you need to use convertRowIndexToView().

For more information about RowSorter, TableRowSorter, and RowFilter see the javadoc for the respective classes:

* RowSorter
* TableRowSorter
* RowFilter

Copyright (c) 2004-2005 Sun Microsystems, Inc.
All Rights Reserved.

No comments: