Advanced Tips for More Powerful Tables
- Build Fancy Column Headers
- Create Progress Cells
- Concluding Thoughts
The previous article introduced four simple tips that you can use to customize the Swing table component. You learned how to assign ToolTips to column headers, change a column's cursor, color a table component's cells, and move columns from the keyboard. This article completes my trilogy of table component articles by introducing you to a pair of advanced tips. Those tips show you how to build fancy column headers and create progress cells.
Build Fancy Column Headers
Are you tired of your table components' lackluster column headers? The table component's lack of support for coloring and stylizing (such as bolding and italicizing) header text, and its inability to align header text in different parts of a column's header certainly seem like liabilities. Imagine what a table component's header might look like if Sun provided such support in JTable. Well, you don't have to imagine. Instead, take a look at Figure 1.
Figure 1 Wouldn't it be nice if the table component supported the capabilities of coloring header text, stylizing that text bold or italic and aligning that text in different parts of a column's header?
Why didn't Sun add support to the table component for coloring, stylizing, and aligning column header text? If Sun added that support (along with support for the million other things that developers would like to see), the table component's various classes would be enormous and resulting objects would consume great quantities of memory. And, as you are about to find out, that support is unnecessary: It is not that hard to provide custom coloring, stylizing, and alignment capabilities for column header text.
Before providing coloring, stylizing, and alignment capabilities, you need to identify the different colors, styles, and alignments that you want to achieve. Suppose that you choose blue, green, red, and white as your colors (with black as the default color); choose bold and italic as your styles (with nonbold and nonitalicplainas the default style); and divide alignment into horizontal and vertical components. For horizontal alignment, you choose left alignment and right alignment (with center alignment as the default). And for vertical alignment, you choose top alignment and bottom alignment (with center alignment as the default).
Having chosen what colors, styles, and alignments to support, you next need to figure out how to present your column header text, with custom color, style, and alignment information, to a table component. It would be best if you could keep that presentation simple. One possibility is to assign a letter to each capability (which you can think of as an option) and prefix a list of options to the header text. Using that scenario, you could interpret "Bil:First header" as choose blue color (B), choose italic style (i), and choose left horizontal alignment (l). (The colon character differentiates the Bil options prefix from the column's header textFirst header.) After some thought, you might come up with the list of option letters that appears in Table 1.
Table 1 Option Letters
Option Letter |
Description |
B |
Color blue |
G |
Color green |
R |
Color red |
W |
Color white |
b |
Style bold |
i |
Style italic |
l |
Horizontal alignment left |
r |
Horizontal alignment right |
u |
Horizontal alignment up (also known as top) |
d |
Horizontal alignment down (also known as bottom) |
NOTE
It would be nice to use t as "top" and b as "bottom." However, because b is already being used for "bold," you can always use u for "up" and d for "down." (It all comes down to the same thing when talking about vertical alignment.)
It is time to put theory into practice. Listing 1 presents source code to a FancyHeaders application that uses a separate option prefix to "fancy up" each column's header text.
Listing 1: FancyHeaders.java
// FancyHeaders.java import java.awt.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.table.*; class FancyHeaders extends JFrame { FancyHeaders (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 = { "lBbd:Name", "Rbi:Address", "uWrb:Age" }; 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" }; int [] ages = { 32, 37, 43, 29 }; int nrows = dtm.getRowCount (); int ncols = dtm.getColumnCount (); for (int i = 0; i < nrows; i++) { dtm.setValueAt (names [i], i, 0); dtm.setValueAt (addresses [i], i, 1); dtm.setValueAt (new Integer (ages [i]), i, 2); } // Create a table using the previously created default table // model. JTable jt = new JTable (dtm); // Create a renderer for displaying fancy headers. FancyHeadersRenderer fhr = new FancyHeadersRenderer (); // Get the column model so we can extract individual columns. TableColumnModel tcm = jt.getColumnModel (); // For each table column, sets its header renderer to the // previously created fancy headers renderer. for (int c = 0; c < ncols; c++) { TableColumn tc = tcm.getColumn (c); tc.setHeaderRenderer (fhr); } // 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 300 vertical pixels. setSize (400, 300); // Display the frame window and all contained // components/containers. setVisible (true); } public static void main (String [] args) { // Create a FancyHeaders object, which creates the GUI. new FancyHeaders ("Fancy Headers"); } } class FancyHeadersRenderer extends JLabel implements TableCellRenderer { private Dimension preferredSize; FancyHeadersRenderer () { // Assign a default preferred size. this (new Dimension (80, 80)); } FancyHeadersRenderer (Dimension preferredSize) { // Save the preferred size at which the JLabel is to be rendered. this.preferredSize = preferredSize; // Paint every pixel in the header's rectangle so no underlying // pixels show through. setOpaque (true); // Set the foreground color to the current Color object assigned // to the TableHeader.foreground property. Text appearing on the // header appears in the foreground color (unless that color is // overridden). setForeground (UIManager.getColor ("TableHeader.foreground")); // Set the background color to the current Color object assigned // to the TableHeader.background property. Pixels behind the text // appearing on the header appear in the background color. setBackground (UIManager.getColor ("TableHeader.background")); // Indirectly set the border to be drawn around each header to // the Border object assigned to the TableHeader.cellBorder // property. Indirection is necessary because the default Border // does not leave enough empty space around its edges. As a // result, portions of those characters that butt up against the // left and right border sides are clipped, which doesn't look // nice. (That happens using the Java look and feel.) Border b = UIManager.getBorder ("TableHeader.cellBorder"); setBorder (new BorderWithEmptyEdges (b)); } public Dimension getPreferredSize () { // Return the dimensions of the JLabel subclass component. Those // dimensions determine the size of each column header. return preferredSize; } public Component getTableCellRendererComponent (JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { boolean isBold = false; // Do not use bolding. boolean isItalic = false; // Do not use italics. Color c = null; // Use Table.foreground color. int halignment = CENTER; // Horizontally center text. int valignment = CENTER; // Vertically center text. // It is always a good idea to verify a type before performing a // cast. if (value instanceof String) { String s = (String) value; // Check for options prefix. int colonIndex = s.indexOf (':'); // If options prefix is found (indicated by presence of // colon) then process options. if (colonIndex != -1) { int i = 0; int ch; // Process options prefix, and set options as // appropriate. while ((ch = s.charAt (i++)) != ':') switch (ch) { case 'b': isBold = true; break; case 'i': isItalic = true; break; case 'B': c = Color.BLUE; break; case 'G': c = Color.GREEN; break; case 'R': c = Color.RED; break; case 'W': c = Color.WHITE; break; case 'l': halignment = LEFT; break; case 'r': halignment = RIGHT; break; case 'u': valignment = TOP; break; case 'd': valignment = BOTTOM; } // Deal with the font style options. if (isBold || isItalic) { int style = Font.PLAIN; if (isBold) style = Font.BOLD; if (isItalic) style = Font.ITALIC; Font f = table.getFont ().deriveFont (style); setFont (f); } // Deal with the color options. if (c != null) setForeground (c); // Deal with the horizontal alignment options. setHorizontalAlignment (halignment); // Deal with the vertical alignment options. setVerticalAlignment (valignment); // Throw away options prefix in preparation to saving // string. s = s.substring (colonIndex + 1); } // Save the string in preparation to rendering. setText (s); } // Return a reference to the current JLabel subclass object. // This object's properties will be accessed when actual // rendering takes place. return this; } } class BorderWithEmptyEdges implements Border { private Border b; BorderWithEmptyEdges (Border b) { this.b = b; } public Insets getBorderInsets (Component c) { // Each JLabel component border requires a small amount of empty // space around its edges. return new Insets (5, 5, 5, 5); } public boolean isBorderOpaque () { return b.isBorderOpaque (); } public void paintBorder (Component c, Graphics g, int x, int y, int width, int height) { b.paintBorder (c, g, x, y, width, height); } }
FancyHeaders accomplishes its "magic" by using a header renderer. Basically, FancyHeaders creates a FancyHeadersRenderer object and uses JTable's setHeaderRenderer() method to establish that object as a common header renderer for all column headers. The secret to understanding how to achieve fancy headers is to understand the FancyHeadersRenderer class.
FancyHeadersRenderer extends the JLabel class and implements the TableCellRenderer interface. That implies that you can call various JLabel methods (either declared in JLabel or inherited from superclasses) to customize the renderer. Also, because the overridden getTableCellRendererComponent() method returns a reference to the current FancyHeadersRenderer object, JLabel's paint() method will be called to do the actual rendering.
FancyHeadersRenderer declares a private preferredSize field. That field, of type Dimension, defines the horizontal and vertical dimensions (in pixels) of all column headers. You have the option of using default dimensions of 80x80 pixels when you call the no-argument FancyHeadersRenderer constructor. (That constructor calls FancyHeadersRenderer(Dimension preferredSize) with a new Dimension object of 80x80 pixels.) Alternatively, by calling FancyHeadersRenderer(Dimension preferredSize), you can specify your own dimensions. Regardless of the dimensions that you choose, FancyHeadersRenderer's overridden getPreferredSize() method returns preferredSize each time that it is called by a layout manager when that layout manager is laying out the component that getTableCellRendererComponent() returns.
NOTE
The table component calls getTableCellRendererComponent() to return a FancyHeadersRenderer component. That is followed by a call to getPreferredSize() to return the preferred layout size of the FancyHeadersRenderer component.
The FancyHeadersRenderer(Dimension preferredSize) constructor performs several tasks:
It saves its preferredSize argument value in the preferredSize private field.
It calls setOpaque() (which it inherits from JComponent) with a Boolean true argument value. That method call informs the table component that each pixel on a column header should be painted. In other words, headers are not transparent: They do not show underlying pixels.
It calls the setForeground() and setBackground() methods to establish the foreground and background colors that will be used by JLabel's paint() method for pixel colors during rendering. Not just any colors are chosen, however. Instead, because it is possible to choose different looks and feels (that is, to choose a different visual appearance, such as Windows or Motif, and a different set of keystrokes that corresponds to a certain visual appearance) for presenting a GUI, it is important that column headers blend in visually with other parts of a table component, for whatever look and feel is in effect. Therefore, the current look and feel's table header component foreground and background colors are obtained by calling an appropriate Swing user interface manager (UIManager) method. The foreground color is obtained by calling UIManager.getColor("TableHeader.foreground"), and the background color is obtained by calling UIManager.getColor("TableHeader.background").
It calls UIManager.getBorder("TableHeader.cellBorder") to obtain the current look and feel's border that surrounds each column header. (That border gives a sunken three-dimensional look to a header.) Instead of calling the setBorder() method to establish that border, however, FancyHeadersRenderer(Dimension preferredSize) creates a BorderWithEmptyEdges object and passes the newly obtained border's reference to that object. A reference to the new BorderWithEmptyEdges object is passed to setBorder(). Why? The answer has to do with leaving a few extra pixels around the left, right, top, and bottom edges of a column header. The default border doesn't leave enough pixels. As a result, for some looks and feels, header text appears partially cut off on the left and right edges.
The getTableCellRenderer() component examines a header's text string for an options prefix and sets an appropriate foreground color (which overrides the previously established foreground color in the constructor), styles, and alignments based on the presence of an options prefix and what options are specified. If an option is not present, a default is chosen. For example, if no color option is specified, the foreground color established in the FancyHeadersRenderer(Dimension preferredSize) constructor is the default foreground color.
An implication of how getTableCellRenderer() examines an option prefix is that you can replace a previously specified option with a subsequently specified option. For example, in a "BR:header text" string, getTableCellRenderer() first would determine that blue is to be the foreground color and then would replace that determination with red (because R is the last specified color option). Therefore, red would become the foreground color for use in rendering some column's header text. Once the appropriate color, styles, and alignments have been set and the actual header text has been saved for subsequent rendering, a reference to the current FancyHeadersRenderer subclass object returns. Its paint() method will be called to perform all subsequent rendering. And that is how you build fancy column headers.