Java tip: How to add zebra background stripes to a JTree

Technologies: Java 5+

Zebra stripes in a graphical user interface (GUI) are subtle background stripes painted behind the rows of a hierarchical list, or tree. They improve the readability of wide tree rows, but the JTree class in Java's Swing doesn't support them. This tip shows how to extend JTree to add zebra background stripes.

Code

The ZebraJTree class below extends JTree in Java's Swing package. It overrides its paintComponent() method to fill the tree's background with alternating stripes before painting the tree icons and values. The Java class also wraps the tree's TreeCellRenderer and TreeCellEditor to set the background color of opaque cell components so that they match the background stripes.

The ZebraJTree class works on all operating systems, with default and application-specific cell renderers and editors, and with nearly all Java look and feel choices. The single known problem is with Sun's Java 6 implementation of the GTK+ look and feel. Limitations in that look and feel block it from properly handling component background colors, preventing zebra stripes from being drawn.

Stripe colors are automatically chosen based upon the tree's background and selection background colors. The stripe colors are automatically updated on changes to the JTree's colors made by the application, by the look and feel, or by the user when they change their OS-wide color theme choices for their GUI.

Usage examples and code explanations follow in the next sections.

A ZebraJTree with light blue zebra stripes

/**
 * A JTree that draws a zebra-striped background.
 */
public class ZebraJTree
    extends javax.swing.JTree
{
    public java.awt.Color rowColors[] = new java.awt.Color[2];
    private boolean drawStripes = false;
 
    public ZebraJTree( )
    {
    }
    public ZebraJTree( java.util.Hashtable<?,?> value )
    {
        super( value );
    }
    public ZebraJTree( Object[] value )
    {
        super( value );
    }
    public ZebraJTree( javax.swing.tree.TreeModel newModel )
    {
        super( newModel );
    }
    public ZebraJTree( javax.swing.tree.TreeNode root )
    {
        super( root );
    }
    public ZebraJTree( javax.swing.tree.TreeNode root,
        boolean asksAllowsChildren )
    {
        super( root, asksAllowsChildren );
    }
    public ZebraJTree( java.util.Vector<?> value )
    {
        super( value );
    }
 
    /** Add zebra stripes to the background. */
    public void paintComponent( java.awt.Graphics g )
    {
        if ( !(drawStripes = isOpaque( )) )
        {
            super.paintComponent( g );
            return;
        }
 
        // Paint zebra background stripes
        updateZebraColors( );
        final java.awt.Insets insets = getInsets( );
        final int w   = getWidth( )  - insets.left - insets.right;
        final int h   = getHeight( ) - insets.top  - insets.bottom;
        final int x   = insets.left;
        int y         = insets.top;
        int nRows     = 0;
        int startRow  = 0;
        int rowHeight = getRowHeight( );
        if ( rowHeight > 0 )
            nRows = h / rowHeight;
        else
        {
            // Paint non-uniform height rows first
            final int nItems = getRowCount( );
            rowHeight = 17; // A default for empty trees
            for ( int i = 0; i < nItems; i++, y+=rowHeight )
            {
                rowHeight = getRowBounds( i ).height;
                g.setColor( rowColors[i&1] );
                g.fillRect( x, y, w, rowHeight );
            }
            // Use last row height for remainder of tree area
            nRows    = nItems + (insets.top + h - y) / rowHeight;
            startRow = nItems;
        }
        for ( int i = startRow; i < nRows; i++, y+=rowHeight )
        {
            g.setColor( rowColors[i&1] );
            g.fillRect( x, y, w, rowHeight );
        }
        final int remainder = insets.top + h - y;
        if ( remainder > 0 )
        {
            g.setColor( rowColors[nRows&1] );
            g.fillRect( x, y, w, remainder );
        }
 
        // Paint component
        setOpaque( false );
        super.paintComponent( g );
        setOpaque( true );
    }
 
    /** Wrap cell renderer and editor to add zebra background stripes. */
    private class RendererEditorWrapper
        implements javax.swing.tree.TreeCellRenderer,
        javax.swing.tree.TreeCellEditor
    {
        public javax.swing.tree.TreeCellRenderer ren = null;
        public javax.swing.tree.TreeCellEditor   ed  = null;
 
        public java.awt.Component getTreeCellRendererComponent(
            javax.swing.JTree tree, Object value,
            boolean selected, boolean expanded,
            boolean leaf, int row, boolean hasFocus )
        {
            final java.awt.Component c =
                ren.getTreeCellRendererComponent(
                tree, value, selected, expanded,
                leaf, row, hasFocus );
            if ( selected || !drawStripes )
                return c;
            if ( !(c instanceof javax.swing.tree.DefaultTreeCellRenderer) )
                c.setBackground( rowColors[row&1] );
            else
                ((javax.swing.tree.DefaultTreeCellRenderer)c).
                setBackgroundNonSelectionColor( rowColors[row&1] );
            return c;
        }
 
        public java.awt.Component getTreeCellEditorComponent(
            javax.swing.JTree tree, Object value,
            boolean selected, boolean expanded,
            boolean leaf, int row )
        {
            final java.awt.Component c =
                ed.getTreeCellEditorComponent(
                tree, value, selected, expanded, leaf, row );
            if ( !selected && drawStripes )
                c.setBackground( rowColors[row&1] );
            return c;
        }
 
        public void addCellEditorListener(
            javax.swing.event.CellEditorListener l )
        {
            ed.addCellEditorListener( l );
        }
        public void cancelCellEditing( )
        {
            ed.cancelCellEditing( );
        }
        public Object getCellEditorValue( )
        {
            return ed.getCellEditorValue( );
        }
        public boolean isCellEditable(
            java.util.EventObject anEvent )
        {
            return ed.isCellEditable( anEvent );
        }
        public void removeCellEditorListener(
            javax.swing.event.CellEditorListener l )
        {
            ed.removeCellEditorListener( l );
        }
        public boolean shouldSelectCell(
            java.util.EventObject anEvent )
        {
            return ed.shouldSelectCell( anEvent );
        }
        public boolean stopCellEditing( )
        {
            return ed.stopCellEditing( );
        }
    }
    private RendererEditorWrapper wrapper = null;
 
    /** Return the wrapped cell renderer. */
    public javax.swing.tree.TreeCellRenderer getCellRenderer( )
    {
        final javax.swing.tree.TreeCellRenderer ren = super.getCellRenderer( );
        if ( ren == null )
            return null;
        if ( wrapper == null )
            wrapper = new RendererEditorWrapper( );
        wrapper.ren = ren;
        return wrapper;
    }
 
    /** Return the wrapped cell editor. */
    public javax.swing.tree.TreeCellEditor getCellEditor( )
    {
        final javax.swing.tree.TreeCellEditor ed = super.getCellEditor( );
        if ( ed == null )
            return null;
        if ( wrapper == null )
            wrapper = new RendererEditorWrapper( );
        wrapper.ed = ed;
        return wrapper;
    }
 
    /** Compute zebra background stripe colors. */
    private void updateZebraColors( )
    {
        if ( (rowColors[0] = getBackground( )) == null )
        {
            rowColors[0] = rowColors[1] = java.awt.Color.white;
            return;
        }
        java.awt.Color sel = javax.swing.UIManager.getColor(
            "Tree.selectionBackground" );
        if ( sel == null )
            sel = java.awt.SystemColor.textHighlight;
        if ( sel == null )
        {
            rowColors[1] = rowColors[0];
            return;
        }
        final float[] bgHSB = java.awt.Color.RGBtoHSB(
            rowColors[0].getRed( ), rowColors[0].getGreen( ),
            rowColors[0].getBlue( ), null );
        final float[] selHSB  = java.awt.Color.RGBtoHSB(
            sel.getRed( ), sel.getGreen( ), sel.getBlue( ), null );
        rowColors[1] = java.awt.Color.getHSBColor(
            (selHSB[1]==0.0||selHSB[2]==0.0) ? bgHSB[0] : selHSB[0],
            0.1f * selHSB[1] + 0.9f * bgHSB[1],
            bgHSB[2] + ((bgHSB[2]<0.5f) ? 0.05f : -0.05f) );
    }
}

Examples

Construct a ZebraJTree, enable editing and root handles, and add the tree to a scroll pane:

ZebraJTree tree = new ZebraJTree( root );
tree.setEditable( true );
tree.setShowsRootHandles( true );
JScrollPane scrollList = new JScrollPane( tree );

Explanation

A Java JTree object is painted in two steps:

  1. If the tree is opaque, the tree's area is painted with a solid background color.
  2. For each tree node, a component is retrieved from the tree's cell renderer or editor and painted.

To add zebra background stripes, both of these steps must be overridden. Overriding background painting enables ZebraJTree to fill the entire tree background with stripes. These stripes are visible if the tree is empty, if it is too short to fill the tree's scroll pane viewport, if it is short because tree nodes have been collapsed, or if the tree's cells are painted with non-opaque components. Overriding the cell renderer and editor enables ZebraJTree to set the background colors for opaque cell components returned by the TreeCellRenderer and TreeCellEditor before they are rendered atop the tree's background stripes.

Painting background stripes on the JTree

Normally, Java's JTree paintComponent() method calls the tree's UI delegate to draw the background and tree components. ZebraJTree overrides paintComponent() and paints alternating background stripes the width of the tree and the height of each tree row. It then temporarily turns off opacity and call's the JTree's paintComponent() method to let the UI delegate paint the tree nodes. With opacity disabled, the UI delegate won't repaint the tree background, leaving the stripes visible.

Zebra background stripes are disabled if the JTree is not opaque.

Painting background stripes on tree nodes

Normally, Java's JTree getCellRenderer() and getCellEditor() methods return a component used to render or edit a tree node. If that component is opaque, it paints its own background before painting the node's value. ZebraJTree overrides getCellRenderer() and getCellEditor() and inserts a wrapper TreeCellRenderer and TreeCellEditor that sets the background color of returned components. When the components are painted, their backgrounds match the tree's stripes. The wrapper should work for any application-specific cell renderer or editor, showing zebra stripes behind any type of tree component.

Setting zebra background stripe colors

There are no standard SystemColor objects or look and feel properties for zebra stripe colors. Instead, ZebraJTree uses the tree's background color for even stripes, and computes a slightly different background color for odd stripes. Both of these colors are updated on each repaint in order to respond to any changes made by the application or by the user through OS-wide theme changes to their GUI.

Dealing with quirks

Quirk #1: Unlike most other selectable Swing components, JTree does not have methods to get or set the selection background color. Instead it relies upon look and feel defaults for itself and cell components. Since ZebraJTree's stripe color algorithm needs the selection background color, it retrieves it from the look and feel's UI defaults.

Quirk #2: JTree's DefaultTreeCellRenderer extends JLabel, but does not honor the label's background color. Instead, it has its own methods to get and set selection and non-selection background colors. ZebraJTree watches for this common case and sets the non-selection color for rendered cell components.

Quirk #3: As of Java 6, Sun's implementation of the GTK+ look and feel for Java does not honor background color choices for tree cell components. This makes it impossible for ZebraJTree to override tree colors and add zebra background stripes.

Testing

This Java class has been tested and works well on the CDE/Motif, Mac OS X, Metal, Windows, and Windows Classic look and feel choices on Linux, Mac, and Windows platforms for Java 5 and Java 6. The automatic background stripe color algorithm works reasonably well for all standard color schemes on these platforms, but you may wish to tune it for your own application's needs.

As noted above, this class does not work with the GTK+ look and feel due to limitations in Sun's implementation.

Further reading

Related tips

Other articles and specifications

Comments

Nice One!!

That is a very good. Good Work!!! :-)

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options

Nadeau software consulting
Nadeau software consulting