Creating Your Own Renderers
One of the most interesting features offered by table components is a table component's ability to customize cell renderering on a column by column basis. Essentially, you attach a custom renderer to a column, and the renderer takes charge of painting that column's cells. For example, you can attach to a favorite color column a renderer that draws images, and that renderer will display those images in the favorite color column's cells. What would the resulting table component look like? Look at Figure 10.
Figure 10 Because a table component supports custom column rendering, you can render images of employee favorite colors in a favorite color column.
The TableDemo10 program that generated Figure 10 was not hard to write. To see what its source code looks like, examine Listing 10.
Listing 10: TableDemo10.java
// TableDemo10.java import java.awt.*; import javax.swing.*; import javax.swing.table.DefaultTableModel; class TableDemo10 extends JFrame { TableDemo10 (String title) { // Pass the title to the JFrame superclass so that it appears in // the title bar. super (title); // Tell the program to exit when the user either selects Close // from the System menu or presses an appropriate X button on the // title bar. setDefaultCloseOperation (EXIT_ON_CLOSE); // Create a my table model consisting of 4 rows by 3 columns. MyTableModel mtm = new MyTableModel (4, 3); // Assign column identifiers (headers) to the columns. String [] columnTitles = { "Name", "Address", "Fav. Color", }; mtm.setColumnIdentifiers (columnTitles); // Populate all cells in the my table model. String [] names = { "John Doe", "Jane Smith", "Jack Jones", "Paul Finch" }; String [] addresses = { "200 Fox Street", "Apt. 555", "Box 9000", "1888 Apple Avenue" }; ImageIcon iiRed = new ImageIcon ("red.gif"); Icon [] favColors = { iiRed, new ImageIcon ("green.gif"), new ImageIcon ("blue.gif"), iiRed }; int nrows = mtm.getRowCount (); for (int i = 0; i < nrows; i++) { mtm.setValueAt (names [i], i, 0); mtm.setValueAt (addresses [i], i, 1); mtm.setValueAt (favColors [i], i, 2); } // Create a table using the previously created my table // model. JTable jt = new JTable (mtm); // Increase the height of each row by 50% so we can see the whole // image. jt.setRowHeight (3 * jt.getRowHeight () / 2); // Place the table in a JScrollPane object (to allow the table to // be vertically scrolled and display scrollbars, as necessary). JScrollPane jsp = new JScrollPane (jt); // Add the JScrollPane object to the frame window's content pane. // That allows the table to be displayed within a displayed scroll // pane. getContentPane ().add (jsp); // Establish the overall size of the frame window to 400 // horizontal pixels by 175 vertical pixels. setSize (400, 175); // Display the frame window and all contained // components/containers. setVisible (true); } public static void main (String [] args) { // Create a TableDemo10 object, which creates the GUI. new TableDemo10 ("Table Demo #10"); } } class MyTableModel extends DefaultTableModel { MyTableModel (int rows, int cols) { super (rows, cols); } public Class getColumnClass (int columnIndex) { // If column is the Fav. Color column, return Icon.class so // that the Icon renderer will be used. if (columnIndex == 2) return Icon.class; else return Object.class; } }
When a JTable object is created, several default renderers are also created. Those renderers are capable of rendering Boolean choices, dates, image icons, numbers, and objects (as strings). If you do not explicitly attach a custom render to a column, a table component chooses a default renderer on your behalf. By default, that renderer renders objects as strings (for all columns). To change to different default renderers, you must override the getColumnClass(int columnIndex) method in your model. That is exactly what TableDemo10 does: It overrides that method in a MyTableModel class.
If you examine MyTableModel's getColumnClass(int columnIndex) method, you will see that it returns the Icon.class Class object when columnIndex equals 2 (the third column). Otherwise, Object.class returns. Returning Icon.class (or ImageIcon.class) for the third column is all that is required to establish an image renderer for that column.
Before image rendering can occur, some images are required. Take a look at TableDemo10's constructor, and you will encounter the following code fragment:
ImageIcon iiRed = new ImageIcon ("red.gif"); Icon [] favColors = { iiRed, new ImageIcon ("green.gif"), new ImageIcon ("blue.gif"), iiRed };
The code fragment creates an array of Icon references to ImageIcon objects, and each ImageIcon object loads a GIF file. Notice that the ImageIcon object reference in iiRed is assigned to entries 0 and 3 in the favColors array. If the same image is going to appear in multiple cells, why load that image multiple times and waste memory storing duplicates? That is why I have chosen to reference the same object from two array entries.
In addition to the above code fragment, mtm.setValueAt (favColors [i], i, 2); is called (in a loop) to assign favColors's Icon references to the third column's cell values in the table component's model. Nothing else needs to be done (except, perhaps, to adjust the height of the rowsby calling JTable's setRowHeight(int rowHeight) methodso the entire image can be displayed). Images now appear in the third column's cells (assuming that the GIF files can be found in the current directory, and no problems occur during ImageIcon's internal file loading activity).
To use other default renderers, you need to know what Class objects to return from getColumnClass(int columnIndex). Table 8 itemizes all default renderers and their associated Class objects.
Table 8 Class Objects and Default Renderers
Class Object |
Default Renderer |
Boolean.class |
Renders a check box with a check mark (to represent true) or no check mark (to represent false). |
Date.class |
Renders a date using a date formatter. (See the DateFormat class and its getDateInstance() and format (Object value) methods for more information.) |
Double.class |
Renders a floating-point value using a numeric formatter. The value is aligned with the right edge of a cell's border. (See the NumberFormat class and its getInstance() and format(Object value) methods for more information.) |
Float.class |
A synonym for Double.class. Same renderer is used. |
Icon.class |
Renders a centered image using an image renderer. |
ImageIcon.class |
A synonym for Icon.class. Same renderer is used. |
Number.class |
Renders any kind of integer or floating-point number in a right-aligned format. |
Object.class |
Converts the contents of any object to a String (by calling the object's toString() method) and renders the String's characters in a left-aligned format. |
The Number.class renderer is interesting in that both integer and floating-point values can be rendered. As a result, you could construct the following array of Number references to objects that are passed to a model (by way of setValueAt(Object v, int rowIndex, int columnIndex), and the numeric renderer would render each number appropriately:
Number [] numbers = { new BigInteger ("8888888888888888888888888888888888888888888"), new BigDecimal ("1e+500"), new Byte ((byte) 10), new Double (3.15), new Float (1.5), new Integer (20), new Long (3000000000L), new Short ((short) 30000) };
Renderers in Depth
Eventually, you will discover that a table component's default renderers do not meet your needs. At that time, you will want to develop a custom renderer. You accomplish that task by doing two things. First, you create a class that implements the TableCellRenderer interface and overrides that interface's getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) method to return a Component reference to an object that performs the actual rendering. Second, you create an object from your class and call TableColumn's setCellRenderer(TableCellRenderer tcr) method to establish that object as a renderer for one or more columns. Alternatively, you can call TableColumn's setHeaderRenderer(TableCellRenderer tcr) method to establish that object as a renderer for one or more column header cells.
Creating a custom renderer is not as difficult as it sounds. For your first taste of a custom renderer, examine the TableDemo11 source code in Listing 11.
Listing 11: TableDemo11.java
// TableDemo11.java import java.awt.*; import javax.swing.*; import javax.swing.table.*; class TableDemo11 extends JFrame { TableDemo11 (String title) { // Pass the title to the JFrame superclass so that it appears in // the title bar. super (title); // Tell the program to exit when the user either selects Close // from the System menu or presses an appropriate X button on the // title bar. setDefaultCloseOperation (EXIT_ON_CLOSE); // Create a default table model consisting of 4 rows by 3 // columns. DefaultTableModel dtm = new DefaultTableModel (4, 3); // Assign column identifiers (headers) to the columns. String [] columnTitles = { "Name", "Address", "Favorite Color", }; dtm.setColumnIdentifiers (columnTitles); // Populate all cells in the default table model. String [] names = { "John Doe", "Jane Smith", "Jack Jones", "Paul Finch" }; String [] addresses = { "200 Fox Street", "Apt. 555", "Box 9000", "1888 Apple Avenue" }; String [] favoriteColors = { "red", "blue", "green", "yellow" }; int nrows = dtm.getRowCount (); for (int i = 0; i < nrows; i++) { dtm.setValueAt (names [i], i, 0); dtm.setValueAt (addresses [i], i, 1); dtm.setValueAt (favoriteColors [i], i, 2); } // Create a table using the previously created default table // model. JTable jt = new JTable (dtm); // Establish a color renderer for the third column. jt.getColumnModel ().getColumn (2). setCellRenderer (new ColorRenderer()); // Place the table in a JScrollPane object (to allow the table to // be vertically scrolled and display scrollbars, as necessary). JScrollPane jsp = new JScrollPane (jt); // Add the JScrollPane object to the frame window's content pane. // That allows the table to be displayed within a displayed scroll // pane. getContentPane ().add (jsp); // Establish the overall size of the frame window to 400 // horizontal pixels by 110 vertical pixels. setSize (400, 110); // Display the frame window and all contained // components/containers. setVisible (true); } public static void main (String [] args) { // Create a TableDemo11 object, which creates the GUI. new TableDemo11 ("Table Demo #11"); } } class ColorRenderer extends JLabel implements TableCellRenderer { ColorRenderer () { // Ensure that all nonforeground pixels are painted in the // background. // color. setOpaque (true); // Switch off JLabel's use of BOLD. setFont (getFont ().deriveFont (Font.PLAIN)); } public Component getTableCellRendererComponent (JTable table, Object value, boolean isSelected, boolean isFocus, int row, int column) { // As a safety check, it's always good to verify the type of // value. if (value instanceof String) { String s = (String) value; // Establish an appropriate background color. That color // remains in effect for all columns to which the // ColorRenderer object // is attached until setBackground(Color c) is called again. if (s.equals ("red")) setBackground (Color.red); else if (s.equals ("green")) setBackground (Color.green); else if (s.equals ("blue")) setBackground (Color.blue); else if (s.equals ("yellow")) setBackground (Color.yellow); else setBackground (Color.white); // Ensure text is displayed horizontally. setHorizontalAlignment (CENTER); // Assign the text to the JLabel component. setText (s); } // Return a reference to the ColorRenderer (JLabel subclass) // component. Behind the scenes, that component will be used to // paint the pixels. return this; } }
TableDemo11 accomplishes the same task as TableDemo10, in that it displays a favorite color in a table component's third column. However, instead of displaying an image of a color in a cell, TableDemo11 establishes a renderer that colors every pixel in that cell and prints the color's name as well, as shown in Figure 11.
Figure 11 A custom renderer displays text over a colored background.
TableDemo11 executes the following method call on JTable reference jt, to assign a custom ColorRenderer renderer object to the table component's third column: jt.getColumnModel ().getColumn (2).setCellRenderer (new ColorRenderer());. The ColorRenderer() constructor ensures that a background color is used to paint all background pixels in a cell by calling JComponent's setOpaque(boolean isOpaque) method. Then ColorRenderer() changes a JLabel's default font style from bold to plain because cell text looks better when it isn't bold (in my opinion).
At some point, the getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) method will be called to return a Component reference to an object that is capable of painting a cell's pixels. Of course, a JTable object reference is passed to that method to facilitate calling JTable methods. The object referenced by value contains the object to appear in the cell. That value doesn't have to be a String. It can be whatever has been assigned to the appropriate column of cell values in the table component's model. The isSelected and hasFocus flags determine whether a cell should be rendered as a selected cell or a focused cell. The cell is rendered as a selected cell if isSelected is true and hasFocus is false. If hasFocus is true, the cell is rendered as a focused cell. Finally, row and column identify the table cell to be rendered. If you attach the same custom renderer to multiple columns, you can use column to distinguish one column from another, in case you want to customize your rendering on a per-column basis. Whether you attach a custom renderer to one or more columns, you can use row to distinguish one row from another, to customize the rendering of different rows.
Inside getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column), you will probably want to validate the type of value to prevent cast problems later. Although it's not really necessary in the TableDemo11 program because the type of all cells assigned to the third column in the table component's model is String and because the custom renderer is assigned to only that column, it is still a good idea to perform that validation (because you might later change the type of the column's values or attach the same custom renderer to multiple columns of different types). In TableDemo11, if value is of the appropriate type, a call is made to a setBackground(Color c) method to choose an appropriate background color. Notice that equals(Object o) is called to compare value's contents (once cast to a String) against "red", "green", "blue", and "yellow", and an appropriate setBackground(Color c) method call occurs if there is a match. If there is no match (because value contains an invalid string of characters, such as redx instead of red), the background color is set to white. After the background color is set, JLabel's setText(String s) method is called to save the String contents (for future rendering), and a reference to the ColorRenderer object returns. That renderer will be used to call the paint(Graphics g, JComponent c) method in JLabel's UI delegate.
TIP
Instead of subclassing JLabel and implementing TableCellRenderer, Sun recommends that you subclass DefaultTableCellRenderer (which already subclasses JLabel and implements TableCellRenderer) whenever you need to use a JLabel as a renderer. Sun recommends doing that because there are performance advantages. To find out what those advantages are, review the SDK documentation for DefaultTableCellRenderer.
How Rendering Works
When a table component's cells must be rendered, the Abstract Windowing Toolkit (AWT) painting system calls JComponent's paint(Graphics g) method. Among its various tasks, paint(Graphics g) calls JComponent's paintComponent(Graphics g) method. In turn, that method calls the UI delegate's update(Graphics g, JComponent c) method. That method results in a call to the UI delegate's paint(Graphics g, JComponent c) method, which (for a table component's UI delegate) calls a paintGrid(Graphics g, int rMin, int rMax, int cMin, int cMax) method (to render a table component's grid) followed by a paintCells(Graphics g, int rMin, int rMax, int cMin, int cMax) method (to render the cells). That latter method calls the UI delegate's paintCell(Graphics g, Rectangle cellRect, int row, int column)and that is where the rendering (for a specific cell) takes place.
When paintCell(Graphics g, Rectangle cellRect, int row, int column) is called, g identifies the graphics context (whose methods will perform the actual rendering), cellRect identifies the location and dimensions of the rectangle in which rendering will occur, row identifies the cell row, and column identifies the cell column. Having the cell's row and column is necessary so that its value can be extracted from the model. The following code fragment (taken from BasicTableUI) presents the source code to paintCell(Graphics g, Rectangle cellRect, int row, int column):
private void paintCell (Graphics g, Rectangle cellRect, int row, int column) { // Painting occurs anytime a component sustains damage. For the // table component, it might be in the process of editing a cell's // contents when a paint request occurs. If the table component is // editing, and if the cell about to be painted is the cell being // edited, have the editor component move and resize itself to the // appropriate cell area of the table component's visual // representation (which will probably result in a repaint of the // editor component) by calling setBounds(), and mark the editor as // valid (so a layout manager will not attempt to perform a layout // operation involving the editor component). if (table.isEditing () && table.getEditingRow () == row && table.getEditingColumn () == column) { Component component = table.getEditorComponent (); component.setBounds (cellRect); component.validate (); } else { // To render a cell, the first thing that is needed is a // renderer. The following method call obtains the renderer // associated with the cell at (row, column). TableCellRenderer renderer = table.getCellRenderer (row, column); // Once a renderer has been obtained, it must be prepared for // use in rendering. The following method call allows the // renderer to select a Component object and configure that // object (by setting the Component's foreground and background // colors, font, and anything else that is appropriate). The // Component object that returns will be used to perform the // rendering. Component component = table.prepareRenderer (rendererer, row, column); // The following renderingPane variable references a // CellRendererPane object. That object's // paintComponent(Graphics g, Component c, int x, int y, int // width, int height, boolean shouldValidate) method is called // to configure the component's location so that the component // is drawn in the right location, mark the component as valid // so a layout manager will not attempt to perform a layout // operation involving the component, and invoke the // component's paint(Graphics g) method to paint the actual // pixels. rendererPane.paintComponent (g, component, table, cellRect.x, cellRect.y, cellRect.width, cellRect.height, true); } }
How does getCellRenderer(int rowIndex, int columnIndex) extract a renderer? The following source code (taken from the JTable class) demonstrates this:
public TableCellRenderer getCellRenderer (int row, int column) { // Acquire a reference to the TableColumn object associated with // column. TableColumn tableColumn = getColumnModel ().getColumn (column); // Fetch the renderer associated with the TableColumn object. TableCellRenderer renderer = tableColumn.getCellRenderer (); // If the developer has not specified a renderer for that column, // obtain a reference to the default renderer. if (renderer == null) renderer = getDefaultRenderer (getColumnClass (column)); return renderer; }
When a JTable object is created, a group of default renderers is installed. Those renderers are capable of rendering objects as strings and rendering numbers, dates, Booleans, and icons. If a developer does not explicitly specify a renderer for a column (by calling TableColumn's setCellRenderer(TableCellRenderer r) method), JTable's getCellRenderer(int rowIndex, int columnIndex) method calls getColumnClass (column) to return a Class object that identifies the type of a column. Using that value, JTable's getDefaultRenderer(Class c) method is called to return a renderer associated with the Class object referenced by c.
Following the call to getCellRenderer(int rowIndex, int columnIndex), paintCell(Graphics g, Rectangle cellRect, int row, int column) calls JTable's prepareRenderer(TableCellRenderer renderer, int row, int column) method to set up the renderer in preparation for rendering, as the following source code demonstrates:
public Component prepareRenderer (TableCellRenderer renderer, int row, int column) { // Contact the model (via JTable's convenience method) to retrieve // the value of the cell at (row, column). Object value = getValueAt (row, column); // Contact the selection model (via JTable's convenience method) to // determine if the cell about to be rendered is a selected cell. If // so, it should be rendered with some kind of selection indicator // (such as the light bluish-purple background that appears under // the Java look and feel, if the cell's type is Object.class). boolean isSelected = isCellSelected (row, column); // Contact the selection model to determine if the cell is selected // but if it represents an anchor cell the first cell of a // selection interval. In that case, the cell must be specially // rendered (such as having a thicker light bluish-purple border and // a white background under the Java look and feel, if the cell's // type is Object.class). boolean rowIsAnchor = (selectionModel.getAnchorSelectionIndex () == row); boolean colIsAnchor = (columnModel.getSelectionModel (). getAnchorSelectionIndex () == column); // If the cell is an anchor selection cell, and if it has the focus, // that cell must be set apart from other selected cells (such as // having a thicker black border and a white background under the // Java look and feel, if the cell's type is Object.class). boolean hasFocus = (rowIsAnchor && colIsAnchor) && isFocusOwner (); // The previously determined values are passed to the following // method. Those values determine how the cell value appears, as // well as selection and focus. return renderer.getTableCellRendererComponent (this, value, isSelected, hasFocus, row, column); }
Once preparation is complete, a Component object returns from prepareRenderer(TableCellRenderer renderer, int row, int column). That object is used to perform the actual painting. The beginning of the painting process starts with rendererPane.paintComponent (g, component, table, cellRect.x, cellRect.y, cellRect.width, cellRect.height, true);. That method call results in the Component's paint(Graphics g) method being called. Assuming the Component reference is actually a JComponent subclass reference (such as a JLabel reference), the paint(Graphics g, JComponent c) method of the component's UI delegate eventually gets called to do the actual painting.
The process by which a table component renders cells is intricate and can come across as being confusing. However, without that process, you would not be able to create custom renderers that paint cells any way you want. Hopefully, you now have a taste for rendering and will be able to create your own custom renderers.