The JTabte component displays a two-dimensional grid of objects. Tables are common in user interfaces, and the Swing team has put a lot of effort into
the table control. Tables are inherently complex, but—perhaps more successfully than other Swing classes—the JTabte component hides much of that complexity. You can produce fully functional tables with rich behavior by writing a few lines of code. You can also write more code and customize the display and behavior for your specific applications.
In the following sections, we will explain how to make simple tables, how the user interacts with them, and how to make some of the most common adjustments. As with the other complex Swing controls, it is impossible to cover all aspects in complete detail. For more information, look in Graphic Java™, Third Edition, by David M. Geary (Prentice Hall, 1999), or Core Swing by Kim Topley (Prentice Hall, 1999).
1. A Simple Table
A JTabte does not store its own data but obtains them from a table model. The JTabte class has a constructor that wraps a two-dimensional array of objects into a default model. That is the strategy that we use in our first example; later in this chapter, we will turn to table models.
Figure 11.1 shows a typical table, describing the properties of the planets of the solar system. (A planet is gaseous if it consists mostly of hydrogen and helium. You should take the “Color” entries with a grain of salt—that column was added because it will be useful in later code examples.)
As you can see from the code in Listing 11.1, the data of the table is stored as a two-dimensional array of Object values:
Object[][] cetts =
{
{ “Mercury”, 2440.0, 0, false, Cotor.YELLOW },
{ “Venus”, 6052.0, 0, false, Color.YELLOW },
…
}
The table simply invokes the toString method on each object to display it. That’s why the colors show up as java.awt.Color[r=. . .,g=. . .,b=. . .].
Supply the column names in a separate array of strings:
String[] columnNames = { “Planet”, “Radius”, “Moons”, “Gaseous”, “Color” };
Then, construct a table from the cell and column name arrays:
var table = new JTable(cells, columnNames);
You can add scroll bars in the usual way—by wrapping the table in a JScrollPane:
var pane = new JScrollPane(table);
When you scroll the table, the table header doesn’t scroll out of view.
Next, click on one of the column headers and drag it to the left or right. See how the entire column becomes detached (see Figure 11.2). You can drop it in a different location. This rearranges the columns in the view only. The data model is not affected.
To resize columns, simply place the cursor between two columns until the cursor shape changes to an arrow. Then, drag the column boundary to the desired place (see Figure 11.3).
Users can select rows by clicking anywhere in a row. The selected rows are highlighted; you will see later how to get selection events. Users can also edit the table entries by clicking on a cell and typing into it. However, in this code example, the edits do not change the underlying data. In your programs, you should either make cells uneditable or handle cell editing events and update your model. We will discuss those topics later in this section.
Finally, click on a column header. The rows are automatically sorted. Click again, and the sort order is reversed. This behavior is activated by the call
tabte.setAutoCreateRowSorter(true);
You can print a table with the call
tabte.print();
2. Table Models
In the preceding example, the table data were stored in a two-dimensional array. However, you should generally not use that strategy in your own code. Instead of dumping data into an array to display it as a table, consider implementing your own table model.
Table models are particularly simple to implement because you can take advantage of the AbstractTableModel class that implements most of the required methods. You only need to supply three methods:
public int getRowCount();
public int getColumnCount();
public Object getValueAt(int row, int column);
There are many ways of implementing the getValueAt method. For example, if you want to display the contents of a RowSet that contains the result of a database query, simply provide this method:
public Object getValueAt(int r, int c)
{
try
{
rowSet.absolute(r + 1);
return rowSet.getObject(c + 1);
}
catch (SQLException e)
{
e.printStackTrace();
return null;
}
}
Our sample program is even simpler. We construct a table that shows some computed values—namely, the growth of an investment under different interest rate scenarios (see Figure 11.4).
The getValueAt method computes the appropriate value and formats it:
public Object getValueAt(int r, int c)
{
double rate = (c + minRate) / 100.0;
int nperiods = r;
double futureBalance = INITIAL_BALANCE * Math.pow(1 + rate, nperiods);
return String.format(“%.2f”, futureBalance);
}
The getRowCount and getColumnCount methods simply return the number of rows and columns:
public int getRowCount() { return years; }
public int getColumnCount() { return maxRate – minRate + 1; }
If you don’t supply column names, the getColumnName method of the AbstractTableMbdel names the columns A, B, C, and so on. To change the default column names, override the getCotumnName method. In this example, we simply label each column with the interest rate.
public String getCotumnName(int c) { return (c + minRate) + “%”; }
You can find the complete source code in Listing 11.2.
3. Working with Rows and Columns
In this subsection, you will see how to manipulate the rows and columns in a table. As you read through this material, keep in mind that a Swing table is quite asymmetric—the operations that you can carry out on rows and columns are different. The table component was optimized to display rows of information with the same structure, such as the result of a database query, not an arbitrary two-dimensional grid of objects. You will see this asymmetry throughout this subsection.
3.1. Column Classes
In the next example, we again display our planet data, but this time we want to give the table more information about the column types. This is achieved by defining the method
Class<?> getColumnClass(int cotumnIndex)
of the table model to return the class that describes the column type.
The JTable class uses this information to pick an appropriate renderer for the class. Table 11.1 shows the default rendering actions.
You can see the checkboxes and images in Figure 11.5. (Thanks to Jim Evins for providing the planet images!)
To render other types, you can install a custom renderer—see Section 11.1.4, “Cell Rendering and Editing,” on p. 639.
3.2. Accessing Table Columns
The JTable class stores information about table columns in objects of type TabteCotumn. A TableColumnModel object manages the columns. (Figure 11.6 shows the relationships among the most important table classes.) If you don’t want to insert or remove columns dynamically, you won’t use the column model much. The most common use for the column model is simply to get a TabteCotumn object:
int columnIndex = . .
TabteCotumn column = table.getColumnModel().getColumn(columnIndex);
3.3. Resizing Columns
The TableColumn class gives you control over the resizing behavior of columns. You can set the preferred, minimum, and maximum width with the methods
void setPreferredWidth(int width)
void setMinWidth(int width)
void setMaxWidth(int width)
This information is used by the table component to lay out the columns. Use the method
void setResizable(boolean resizable)
to control whether the user is allowed to resize the column.
You can programmatically resize a column with the method
void setWidth(int width)
When a column is resized, the default is to leave the total size of the table unchanged. Of course, the width increase or decrease of the resized column must then be distributed over other columns. The default behavior is to change the size of all columns to the right of the resized column. That’s a good default because it allows a user to adjust all columns to a desired width, moving from left to right.
You can set another behavior from Table 11.2 by using the method
void setAutoResizeMode(int mode)
of the JTabte class.
3.4. Resizing Rows
Row heights are managed directly by the JTabte class. If your cells are taller than the default, you may want to set the row height:
tabte.setRowHeight(height);
By default, all rows of the table have the same height. You can set the heights of individual rows with the call
table.setRowHeight(row, height);
The actual row height equals the row height set with these methods, reduced by the row margin. The default row margin is 1 pixel, but you can change it with the call
table.setRowMargin(margin);
3.5. Selecting Rows, Columns, and Cells
Depending on the selection mode, the user can select rows, columns, or individual cells in the table. By default, row selection is enabled. Clicking inside a cell selects the entire row (see Figure 11.5). Call
tabte.setRowSelectionAllowed(fatse);
to disable row selection.
When row selection is enabled, you can control whether the user is allowed to select a single row, a contiguous set of rows, or any set of rows. You need to retrieve the selection model and use its setSetectionMode method:
table.getSetectionModel().setSetectionMode(mode);
Here, mode is one of the three values:
ListSetectionModet.SINGLE_SELECTION
ListSetectionModet.SINGLE_INTERVAL_SELECTION
ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
Column selection is disabled by default. You can turn it on with the call
tabte.setCotumnSetectionAllowed(true);
Enabling both row and column selection is equivalent to enabling cell selection. The user then selects ranges of cells (see Figure 11.7). You can also enable that setting with the call
tabte.setCellSetectionEnabted(true);
Run the program in Listing 11.3 to watch cell selection in action. Enable row, column, or cell selection in the Selection menu and watch how the selection behavior changes.
You can find out which rows and columns are selected by calling the getSelectedRows and getSelectedColumns methods. Both return an int[] array of the indexes of the selected items. Note that the index values are those of the table view, not the underlying table model. Try selecting rows and columns, then drag columns to different places and sort the rows by clicking on column headers. Use the Print Selection menu item to see which rows and columns are reported as selected.
If you need to translate the table index values to table model index values, use the JTabte methods convertRowIndexToModet and convertCotumnIndexToModet.
As you have seen in our first table example, it is easy to add row sorting to a JTabte simply by calling the setAutoCreateRowSorter method. However, to have finer-grained control over the sorting behavior, install a TabteRowSorter<M> object into a JTabte and customize it. The type parameter M denotes the table model; it needs to be a subtype of the TabteModet interface.
var sorter = new TabteRowSorter<TabteModet>(modet);
tabte.setRowSorter(sorter);
Some columns should not be sortable, such as the image column in our planet data. Turn sorting off by calling
sorter.setSortable(IMAGE_COLUMN, false);
You can install a custom comparator for each column. In our example, we will sort the colors in the Color column by preferring blue and green over red. When you click on the Color column, you will see that the blue planets go to the bottom of the table. This is achieved with the following call:
sorter.setComparator(COLOR_COLUMN, new Comparator<Color>()
{
public int compare(Cotor c1, Color c2)
{
int d = c1.getBlue() – c2.getBlue();
if (d != 0) return d;
d = c1.getGreen() – c2.getGreen();
if (d != 0) return d;
return c1.getRed() – c2.getRed();
}
});
If you do not specify a comparator for a column, the sort order is determined as follows:
- If the column class is String, use the default collator returned by Collator.getInstance(). It sorts strings in a way that is appropriate for the current locale. (See Chapter 7 for more information about locales and collators.)
- If the column class implements Comparable, use its compareTo method.
- If a TableStringConverter has been set for the sorter, sort the strings returned by the converter’s toString method with the default collator. If you want to use this approach, define a converter as follows:
sorter.setStringConverter(new TableStringConverter()
{
public String toString(TableModel model, int row, int column)
{
Object value = model.getValueAt(row, column);
convert value to a string and return it
}
});
- Otherwise, call the toString method on the cell values and sort them with the default collator.
3.7. Filtering Rows
In addition to sorting rows, the TableRowSorter can also selectively hide rows—a process called filtering. To activate filtering, set a RowFilter. For example, to include all rows that contain at least one moon, call
sorter.setRowFilter(RowFilter.numberFilter(ComparisonType.NOT_EQUAL, 0, MOONS_COLUMN));
Here, we use a predefined number filter. To construct a number filter, supply
- The comparison type (one of EQUAL, NOT_EQUAL, AFTER, or BEFORE).
- An object of a subclass of Number (such as an Integer or Double). Only objects that have the same class as the given Number object are considered.
- Zero or more column index values. If no index values are supplied, all columns are searched.
The static RowFilter.dateFilter method constructs a date filter in the same way; you need to supply a Date object instead of the Number object.
Finally, the static RowFilter.regexFilter method constructs a filter that looks for strings matching a regular expression. For example,
sorter.setRowFilter(RowFilter.regexFilter(“.*[^s]$”, PLANET_COLUMN));
only displays those planets whose name doesn’t end with an “s”. (See Chapter 2 for more information on regular expressions.)
You can also combine filters with the andFilter, orFilter, and notFilter methods. To filter for planets not ending in an “s” with at least one moon, you can use this filter combination:
sorter.setRowFilter(RowFilter.andFilter(List.of(
RowFilter.regexFilter(“.*[^s]$”, PLANET_COLUMN),
RowFilter.numberFilter(ComparisonType.NOT_EQUAL, 0, MOONS_COLUMN))));
To implement your own filter, provide a subclass of RowFilter and implement an include method to indicate which rows should be displayed. This is easy to do, but the glorious generality of the RowFilter class makes it a bit scary.
The RowFilter<M, I> class has two type parameters—the types for the model and for the row identifier. When dealing with tables, the model is always a subtype of TableModel and the identifier type is Integer. (At some point in the future, other components might also support row filtering. For example, to filter rows in a JTree, one might use a RowFilter<TreeModel, TreePath>.)
A row filter must implement the method
public boolean include(RowFilter.Entry<? extends M, ? extends I> entry)
The RowFilter.Entry class supplies methods to obtain the model, the row identifier, and the value at a given index. Therefore, you can filter both by row identifier and by the contents of the row.
For example, this filter displays every other row:
var filter = new RowFilter<TableModel, Integer>()
{
public boolean include(Entry<? extends TableModel, ? extends Integer> entry)
{
return entry.getIdentifier() % 2 == 0;
}
};
If you wanted to include only those planets with an even number of moons, you would instead test for
((Integer) entry.getValue(MOONS_COLUMN)) % 2 == 0
In our sample program, we allow the user to hide arbitrary rows. We store the hidden row indexes in a set. The row filter shows all rows whose indexes are not in that set.
The filtering mechanism wasn’t designed for filters with criteria changing over time. In our sample program, we keep calling
sorter.setRowFilter(filter);
whenever the set of hidden rows changes. Setting a filter causes it to be applied immediately.
3.8. Hiding and Displaying Columns
As you saw in the preceding section, you can filter table rows by either their contents or their row identifier. Hiding table columns uses a completely different mechanism.
The removeColumn method of the JTable class removes a column from the table view. The column data are not actually removed from the model—they are just hidden from view. The removeColumn method takes a TableColumn argument. If you have the column number (for example, from a call to getSelectedColumns), you need to ask the table model for the actual table column object:
TableColumnModel columnModel = table.getColumnModel();
TableColumn column = columnModel.getColumn(i);
table.removeColumn(column);
If you remember the column, you can later add it back in:
table.addColumn(column);
This method adds the column to the end. If you want it to appear elsewhere, call the moveColumn method.
You can also add a new column that corresponds to a column index in the table model, by adding a new TableColumn object:
table.addColumn(new TableColumn(modelColumnIndex));
You can have multiple table columns that view the same column of the model.
The program in Listing 11.3 demonstrates selection and filtering of rows and columns.
4. Cell Rendering and Editing
As you saw in Section 11.1.3.2, “Accessing Table Columns,” on p. 623, the column type determines how the cells are rendered. There are default render- ers for the types Boolean and Icon that render a checkbox or icon. For all other types, you need to install a custom renderer.
4.1. Rendering Cells
Table cell renderers are similar to the list cell renderers that you saw earlier. They implement the TableCellRenderer interface that has a single method:
Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column)
That method is called when the table needs to draw a cell. You return a component whose paint method is then invoked to fill the cell area.
The table in Figure 11.8 contains cells of type Color. The renderer simply returns a panel with a background color that is the color object stored in the cell. The color is passed as the value parameter.
class ColorTableCellRenderer extends JPanel implements TableCellRenderer
{
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column)
{
setBackground((Color) value);
if (hasFocus)
setBorder(UIManager.getBorder(“Table.focusCellHighlightBorder”));
else
setBorder(null);
return this;
}
}
As you can see, the renderer draws a border when the cell has focus. (We ask the UIManager for the correct border. To find the lookup key, we peeked into the source code of the DefaultTableCettRenderer class.)
You need to tell the table to use this renderer with all objects of type Color. The setDefaultRenderer method of the JTable class lets you establish this association. Supply a Class object and the renderer:
table.setDefaultRenderer(Color.class, new ColorTableCellRenderer());
That renderer is now used for all objects of the given type in this table.
If you want to select a renderer based on some other criterion, you need to subclass the JTable class and override the getCellRenderer method.
4.2. Rendering the Header
To display an icon in the header, set the header value:
moonColumn.setHeaderValue(new ImageIcon(“Moons.gif”));
However, the table header isn’t smart enough to choose an appropriate ren- derer for the header value. You have to install the renderer manually. For example, to show an image icon in a column header, call
moonColumn.setHeaderRenderer(tabte.getDefaultRenderer(ImageIcon.ctass));
4.3. Editing Cells
To enable cell editing, the table model must indicate which cells are editable by defining the isCettEditabte method. Most commonly, you will want to make certain columns editable. In the example program, we allow editing in four columns.
public boolean isCettEditabte(int r, int c)
{
return c == PLANET_COLUMN || c == MOONS_COLUMN || c == GASEOUS_COLUMN
|| c == COLOR_COLUMN;
}
If you run the program (Listings 11.4 to 11.7), note that you can click the checkboxes in the Gaseous column and turn the check marks on and off. If you click a cell in the Moons column, a combo box appears (see Figure 11.9). You will shortly see how to install such a combo box as a cell editor.
Finally, click a cell in the first column. The cell gains focus. You can start typing, and the cell contents change.
What you just saw in action are the three variations of the DefaultCellEditor class. A DefaultCellEditor can be constructed with a JTextField, a JCheckBox, or a JComboBox. The JTable class automatically installs a checkbox editor for Boolean cells and a text field editor for all editable cells that don’t supply their own renderer. The text fields let the user edit the strings that result from applying toString to the return value of the getValueAt method of the table model.
When the edit is complete, the edited value is retrieved by calling the getCellEditorValue method of your editor. That method should return a value of the correct type (that is, the type returned by the getColumnType method of the model).
To get a combo box editor, set a cell editor manually—the JTabte component has no idea what values might be appropriate for a particular type. For the Moons column, we wanted to enable the user to pick any value between 0 and 20. Here is the code for initializing the combo box:
var moonCombo = new JComboBox();
for (int i = 0; i <= 20; i++)
moonCombo.addltem(i);
To construct a DefauttCettEditor, supply the combo box in the constructor:
var moonEditor = new DefauttCettEditor(moonCombo);
Next, we need to install the editor. Unlike the color cell renderer, this editor does not depend on the object type—we don’t necessarily want to use it for all objects of type Integer. Instead, we need to install it into a particular column:
moonCotumn.setCettEditor(moonEditor);
4.4. Custom Editors
Run the example program again and click a color. A color chooser pops up and lets you pick a new color for the planet. Select a color and click OK. The cell color is updated (see Figure 11.10).
The color cell editor is not a standard table cell editor but a custom implementation. To create a custom cell editor, implement the TableCettEditor interface. That interface is a bit tedious, and as of Java SE 1.3, an AbstractCellEditor class is provided to take care of the event handling details.
The getTableCellEditorComponent method of the TableCettEditor interface requests a component to render the cell. It is exactly the same as the getTableCellRendererComponent method of the TableCellRenderer interface, except that there is no focus parameter. When the cell is being edited, it is presumed to have focus. The editor component temporarily replaces the renderer when the editing is in progress. In our example, we return a blank panel that is not colored. This is an indication to the user that the cell is currently being edited.
Next, you want to have your editor pop up when the user clicks on the cell.
The JTabte class calls your editor with an event (such as a mouse click) to find out if that event is acceptable to initiate the editing process. The AbstractCettEditor class defines the method to accept all events.
public boolean isCellEditable(EventObject anEvent)
{
return true;
}
However, if you override this method to return false, the table would not go through the trouble of inserting the editor component.
Once the editor component is installed, the shouldSelectCell method is called, presumably with the same event. You should initiate editing in this method—for example, by popping up an external edit dialog box.
public boolean shouldSelectCell(EventObject anEvent)
{
colorDialog.setVisible(true);
return true;
}
If the user cancels the edit, the table calls the cancelCellEditing method. If the user has clicked on another table cell, the table calls the stopCellEditing method. In both cases, you should hide the dialog box. When your stopCellEditing method is called, the table would like to use the partially edited value. You should return true if the current value is valid. In the color chooser, any value is valid. But if you edit other data, you can ensure that only valid data are retrieved from the editor.
Also, you should call the superclass methods that take care of event firing—otherwise, the editing won’t be properly canceled.
public void cancelCellEditing()
{
colorDialog.setVisible(false);
super.cancelCellEditing();
}
Finally, you need a method that yields the value that the user supplied in the editing process:
public Object getCellEditorValue()
{
return colorChooser.getColor();
}
To summarize, your custom editor should do the following:
- Extend the AbstractCettEditor class and implement the TabteCettEditor interface.
- Define the getTabteCettEditorComponent method to supply a component. This can either be a dummy component (if you pop up a dialog box) or a component for in-place editing such as a combo box or text field.
- Define the shoutdSetectCett, stopCettEditing, and cancetCettEditing methods to handle the start, completion, and cancellation of the editing process. The stopCettEditing and cancetCettEditing methods should call the superclass methods to ensure that listeners are notified.
- Define the getCettEditorVatue method to return the value that is the result of the editing process.
Finally, indicate when the user is finished editing by calling the stopCettEditing and cancetCettEditing methods. When constructing the color dialog box, we install the accept and cancel callbacks that fire these events.
cotorDiatog = JCotorChooser.createDiatog(nutt, “Ptanet Cotor”, fatse, cotorChooser,
EventHandter.create(ActionListener.ctass, this, “stopCettEditing”),
EventHandter.create(ActionListener.ctass, this, “cancetCettEditing”));
This completes the implementation of the custom editor.
You now know how to make a cell editable and how to install an editor. There is one remaining issue—how to update the model with the value that the user edited. When editing is complete, the JTabte class calls the following method of the table model:
void setVatueAt(Object vatue, int r, int c)
You need to override the method to store the new value. The vatue parameter is the object that was returned by the cell editor. If you implemented the cell editor, you know the type of the object you return from the getCettEditorVatue method. In the case of the DefauttCettEditor, there are three possibilities for that value. It is a Bootean if the cell editor is a checkbox, a string if it is a text field, and, if the value comes from a combo box, it is the object that the user selected.
If the vatue object does not have the appropriate type, you need to convert it. That happens most commonly when a number is edited in a text field. In our example, we populated the combo box with Integer objects so that no conversion is necessary.
Source: Horstmann Cay S. (2019), Core Java. Volume II – Advanced Features, Pearson; 11th edition.