ORIGINAL DRAFT

Sorted table columns are a common idiom in modern software, yet the Swing JTable model does not directly support this function. This month, we’ll be demonstrating a JTable extension we’ll call JSortTable that supports column sorting and provides the kind of visual feedback that users have become accustomed to, a small arrow icon, visible in the sorted column, pointing in the direction in which the column is sorted. We’ll support both ascending and decending sorts by extending the TableModel with a SortTableModel.

Figure 1: JSortTable with the second column sorted.
The small arrow icon indicates ascending order. Clicking on the column header again will toggle the sorting order.

Figure 1: JSortTable with the second column sorted. The small arrow icon indicates ascending order. Clicking on the column header again will toggle the sorting order.

JSortTable uses a model that extends the standard Swing TableModel, adding a pair of methods that let us check if a column is sortable and call into the model to sort a specified column in the specified order. The interface looks like this:

public interface SortTableModel
  extends TableModel
{
  public boolean isSortable(int col);
  public void sortColumn(
    int col, boolean ascending);
}

To make things as easy as possible, we’ll provide a DefaultTableModel extension that implements this interface. Figure 2 shows the class relationships for this project. I’ve drawn the Swing TableModel and JTable classes in a lighter shade to show that they are not directly part of the project. I haven’t bothered showing that DefaultSortedTableModel extends the Swing DefaultTableModel. Any implementation of the SortTableModel interface would have be suitable here.

Figure 2: JSortTable uses a SortTableModel and
we provide a default implementation to accomodate most purposes.

Figure 2: JSortTable uses a SortTableModel and we provide a default implementation to accomodate most purposes.

We try to extend as few Swing classes as possible. SortTableModel neccesarily extends the TableModel interface but the DefaultSortedTableModel extends DefaultTableModel for convenience. JSortTable extends JTable so that it can be used as a plug-in replacement for any JTable components, inheriting all of the standard behavior.

Most of our work revolves around either sorting, including how to trigger column sorts, and providing suitable visual cues in the header display. We’ll trigger sorts when a header is selected with the mouse and decide whether sorting is ascending or decending based on whether this is the first sort on a given column. In other words, a second click on an already sorted column will toggle the sorting order. We’ll provide a custom renderer that gets configured exactly the same way as the standard JTable header renderer, so this solution is entirely plaftorm-agnostic, providing the right style for any of the Swing look-and-feel slections.

Listing 1 shows the DefaultSortTableModel class, which extends Swing’s DefaultTableModel and implements our new SortTableModel by adding two methods. Most of the code is taken up by constructors that match the parent class, so that each of the beahviors supported by DefaultTableModel are available. The isSortable method always return true in this case, becase all of the columns are sortable. If you were dealing with a table that had some sortable and some non-sortable columns, this method would provide different answers, depending on the column value.

The sortColumn method does the actual sorting, which depends on the Collections sort method and a custom ColumnComparator that demonstrates how you can deal with columns more specifically. The DefaultTableModel handles it’s structures as a set of nested Vector objects. One for the table itself and other Vector objects for each row in the table. Our ColumnComparator implements the compareTo method and compares against the specified column index for each row in the table. By sorting at this level, the rows move into new positions when the table is sorted. It’s important to keep each row in appropriate order, based on the column being sorted.

It’s also important to note the flexibility of the interface and the specificity of the implementation. DefaultSortTableModel handles in-memory table models, effectively the same way as the DefaultTableModel does, but is likely not suitable for larger sets with more rows than would fit into memory. It is, however, quite possible to query a database and to display portions of the table, sorted on appropriate keys by using the SortTableModel interface. In such cases, I suspect that only indexed columns would be sorted but you can return approrpiate values in the isSortable method to control the user interface.

Listing 2 shows the SortHeaderRenderer class, which extends the standard Swing DefaultTableCellRenderer. This class is responsible for drawing the table headers and associated arrows. Because the same icons are used repeatedly, they are initalized once and referenced as static variables. The constructor sets the text position and horizontal alignment, which in turn controls the position of the icon. In this case, the icon is placed at the right of the text, and the text/icon combination is centered in the header view.

The getTableCellRendererComponent does the real work. This code is largely based on the way JTable handles headers, relying on the TableHeader.cellBorder value for the border, so that the implementation reflects the current platform settings. If the table is a JSortTable, we fugure out which column is being sorted and whether we are sorting on ascending or decending order. This information is used later to decide which icon to draw. The middle part of this code sets the foreground and background color, and the font, based on the active JTableHeader, again ensuring that we reflect the current look-and-fell selection.

Listing 3 shows the code for JSortTable, which is the central class in this project. For backward compatibility, we extend JTable and implement each of the supported contructors. That makes JSortTable a drop-in replacement for any JTable component. In fact, if you use the default model and all of your elements implement the Comparable interface, no changes are necessary to get the sorting functionality.

We are interested in the selected column and the sort order, so there are two new instance variables (not inherited from the parent class). The methods you saw earlier in the SortHeaderRenderer code merely return values for these two variables.

Because we are doing common initialization in several constructors, the initSortHeader sets things up for us, primarily by setting the SortheaderRenderer and adding a mouse listener to respond to mouse events in the header. I chose to implement mouse handling in the main class but a subclass of JTableHeader could also have been used. I decided that fewer classes would make it easier to maintain the functionality and that there was little enough code involved to justify putting it directly in JSortTable.

We implement the MouseListener interface but are primarily interested in responding to specific mouse events, specificially in the mouseReleased method. This defers sorting to after the user has released the mouse, which may be after changing the order of the headers by dragging them to new positions. This is important in that responding to mouse pressed events leads to unexpected results if the headers are movable. You’ll also notice that we keep tabs on the index of the column under the cursor but that we also keep in mind that the actual column to be sorted is the column that this maps to in the model.

Most of the mouseReleased method gets positional information and callss the sortColumn method when appropriate, providing suitable arguments. We don’t call sortColumn if the isSortable method returns false. The rest of the code manages our internal variables, carefully saving their values after other calculations have been done. For example, the sortedColumnIndex is assigned only after the ascending order is determined because the decision relies on the previous state of the sorting for a given column, toggling the sort order if the same column is clicked on twice in a row.

Sorted table columns are a common occurence, unfortunately not directly suppoted by Swing, although it is discussed as one of the advanced idioms in Sun’s Java Look and Feel Guidelines. It’s clear that Swing designers have provideed enough flexibility to make it relative easy to implement something like this, but a built-in solution might be a nice addition. While you may not get this for free, you’re now equiped with the tools you need to apply this technique in your programs.

Listing 1

import java.util.*;
import javax.swing.table.*;

public class DefaultSortTableModel
  extends DefaultTableModel
  implements SortTableModel
{
  public DefaultSortTableModel() {}
  
  public DefaultSortTableModel(int rows, int cols)
  {
    super(rows, cols);
  }
  
  public DefaultSortTableModel(Object[][] data, Object[] names)
  {
    super(data, names);
  }
  
  public DefaultSortTableModel(Object[] names, int rows)
  {
    super(names, rows);
  }
  
  public DefaultSortTableModel(Vector names, int rows)
  {
    super(names, rows);
  }
  
  public DefaultSortTableModel(Vector data, Vector names)
  {
    super(data, names);
  }
  
  public boolean isSortable(int col)
  {
    return true;
  }
  
  public void sortColumn(int col, boolean ascending)
  {
    Collections.sort(getDataVector(),
      new ColumnComparator(col, ascending));
  }
}

Listing 1

import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;

public class SortHeaderRenderer
  extends DefaultTableCellRenderer
{
  public static Icon NONSORTED =
    new SortArrowIcon(SortArrowIcon.NONE);
  public static Icon ASCENDING =
    new SortArrowIcon(SortArrowIcon.ASCENDING);
  public static Icon DECENDING =
    new SortArrowIcon(SortArrowIcon.DECENDING);
  
  public SortHeaderRenderer()
  {
    setHorizontalTextPosition(LEFT);
    setHorizontalAlignment(CENTER);
  }
  
  public Component getTableCellRendererComponent(
    JTable table, Object value, boolean isSelected,
    boolean hasFocus, int row, int col)
  {
    int index = -1;
    boolean ascending = true;
    if (table instanceof JSortTable)
    {
      JSortTable sortTable = (JSortTable)table;
      index = sortTable.getSortedColumnIndex();
      ascending = sortTable.isSortedColumnAscending();
    }
    if (table != null)
    {
      JTableHeader header = table.getTableHeader();
      if (header != null)
      {
        setForeground(header.getForeground());
        setBackground(header.getBackground());
        setFont(header.getFont());
      }
    }
    Icon icon = ascending ? ASCENDING : DECENDING;
    setIcon(col == index ? icon : NONSORTED);
    setText((value == null) ? "" : value.toString());
    setBorder(UIManager.getBorder("TableHeader.cellBorder"));
    return this;
  }
}

Listing 3

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JSortTable extends JTable
  implements MouseListener
{
  protected int sortedColumnIndex = -1;
  protected boolean sortedColumnAscending = true;
  
  public JSortTable()
  {
    this(new DefaultSortTableModel());
  }
  
  public JSortTable(int rows, int cols)
  {
    this(new DefaultSortTableModel(rows, cols));
  }
  
  public JSortTable(Object[][] data, Object[] names)
  {
    this(new DefaultSortTableModel(data, names));
  }
  
  public JSortTable(Vector data, Vector names)
  {
    this(new DefaultSortTableModel(data, names));
  }
  
  public JSortTable(SortTableModel model)
  {
    super(model);
    initSortHeader();
  }

  public JSortTable(SortTableModel model,
    TableColumnModel colModel)
  {
    super(model, colModel);
    initSortHeader();
  }

  public JSortTable(SortTableModel model,
    TableColumnModel colModel,
    ListSelectionModel selModel)
  {
    super(model, colModel, selModel);
    initSortHeader();
  }

  protected void initSortHeader()
  {
    JTableHeader header = getTableHeader();
    header.setDefaultRenderer(new SortHeaderRenderer());
    header.addMouseListener(this);
  }

  public int getSortedColumnIndex()
  {
    return sortedColumnIndex;
  }
  
  public boolean isSortedColumnAscending()
  {
    return sortedColumnAscending;
  }
  
  public void mouseReleased(MouseEvent event)
  {
    TableColumnModel colModel = getColumnModel();
    int index = colModel.getColumnIndexAtX(event.getX());
    int modelIndex = colModel.getColumn(index).getModelIndex();
    
    SortTableModel model = (SortTableModel)getModel();
    if (model.isSortable(modelIndex))
    {
      // toggle ascension, if already sorted
      if (sortedColumnIndex == index)
      {
        sortedColumnAscending = !sortedColumnAscending;
      }
      sortedColumnIndex = index;
    
      model.sortColumn(modelIndex, sortedColumnAscending);
    }
  }
  
  public void mousePressed(MouseEvent event) {}
  public void mouseClicked(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
}