Making a JTreeCellRenderer fill the JTree

June 2, 2008

Making a JTreeCellRenderer fill to the right edge of a JTree is a herculean task. Something as seemingly innocuous as controlling a cell renderer’s width turns out to be a rather tough job.

Having a cell renderer fill to the right edge of the JTree is useful if you want to have some left aligned content, as well as some right aligned content. By default, renderer’s are queried for their preferred width, so you’ll end up with arbitrarily sized renders like the one’s below (the cell renderer has a red LineBorder for demonstration):

Clearly this won’t work if our renderer wants anything aligned with the right edge of the tree. The most straight forward way I found to override this behavior is by subclassing (something I don’t like to do) BasicTreeUI and overriding this method:

AbstractLayoutCache.NodeDimensions createNodeDimensions()

This method determines how to size a node. The default implementation bases the calculation around the renderer’s preferred size. Sub-classing BasicTreeUI let’s us determine this value ourselves, and we’re in a spot in the code that has access to all the information we need (like node indentation values).

Note that we actually want the renderer to extend to the right edge of the enclosing JScrollPane rather than the actual JTree.

Here’s the code:

public class CustomTreeUI extends BasicTreeUI {

        @Override
        protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
            return new NodeDimensionsHandler() {
                @Override
                public Rectangle getNodeDimensions(
                        Object value, int row, int depth, boolean expanded,
                        Rectangle size) {
                    Rectangle dimensions = super.getNodeDimensions(value, row,
                            depth, expanded, size);
                    dimensions.width =
                            fScrollPane.getWidth() - getRowX(row, depth);
                    return dimensions;
                }
            };
        }

        @Override
        protected void paintHorizontalLine(Graphics g, JComponent c,
                                           int y, int left, int right) {
            // do nothing.
        }

        @Override
        protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds,
                                              Insets insets, TreePath path) {
            // do nothing.
        }
}

Notice that I’m overriding a couple additional methods to prevent the painting of the node-connecting lines. Also, not shown here, I have set my tree to use a large model by calling Tree.setLargeModel(trure). I’ve yet to figure out exactly what all the implications of setting this property are, but it does seem to cause the tree nodes to be relayed out on each repaint. This is important because the renderer widths will change with the size of the containing JScrollPane, whereas normally, the renderer widths are relatively static on a per-node basis.

The above solution works in my case because I’m providing all the painting (to include the disclosure icons) for my JTree. If you know of more componentized way (that is, not involving sub-classing) of accomplishing this, let me know!

30 Responses to “Making a JTreeCellRenderer fill the JTree”

  1. Felix Says:

    I’ve got a general question about the JTree:
    Is the gradient standard since Leopard? I am just wondering because I fear I would have to implement this on my own to get it running for Tiger, too.

  2. Ken Says:

    Hi Felix,

    I added the gradient myself…so it isn’t standard. It would be great if there were a client property that let us turn that on – though that wouldn’t help you in Tiger.

    I’ve found the most straight forward way to implement this is by overriding the paintComponent method (again, not something I like to do). Here, you can determine if there is a selection, get the bounds of the selected row, and then fill that rectangle with a gradient.

    This technique allows you to fill the *entire* width of the JTree. To handle mouse clicks on the entire width, you’d also have to add your own MouseListener.

    -Ken


  3. […] in the application code, and Ken Orr shows how to address a common problem with tree renderers – making them fill the entire width of the […]

  4. Ken Says:

    I just found a minor bug in my code: dimesions.width should use fScrollPane.getViewport’s width rather than fScrollPane’s (see below). Without this fix, the right side of the tree will be obscured by the vertical scrollbar when it’s showing.

    dimensions.width = fScrollPane.getViewport().getWidth() – getRowX(row, depth);

  5. Vaclav Says:

    Hello, I have a problem with the code above. Although the highlight goes to the right border of the JTree, it does not fill whole line to the left from the node label. Is any special cell renderer required?

  6. Ken Says:

    Hi Vaclav,

    As demonstrated by the red border in the screen shot above, I’ve only intended to have the tree cell fill right. If you’re trying to paint the selection, I’d suggest overriding paintComponent.

    I can post the code for that if you’d like.

    -Ken

  7. Vaclav Says:

    Thanks Ken, the code would be nice if you have time. What paintComponent method should I override? The method of the element that is used for rendering the cell node (provided by TreeCellRenderer)? That would mean painting outside of its boundaries. Is that OK?

  8. Ken Says:

    In JTree, override paintComponent like this (I’ve edited the code a little in this comment, so I can’t guarantee it will compile):

    @Override
    protected void paintComponent(Graphics g) {
    Graphics2D graphics2D = (Graphics2D) g.create();
    fBackgroundPainter.paint(graphics2D,this,getWidth(),getHeight());
    graphics2D.dispose();

    // paint the background for the selected entry, if there is one.
    // NOTE: this code assumes there is only one selected row, but
    // you could loop over the selected row indicies if desired.
    int selectedRow = getSelectionModel().getLeadSelectionRow();
    if (selectedRow > 0 && isVisible(getPathForRow(selectedRow))) {
    graphics2D = (Graphics2D) g.create();
    graphics2D.translate(0, bounds.y);

    // get the bounds of the selected row.
    Rectangle bounds = getRowBounds(selectedRow);

    // create a GraidentPaint here and set the graphics context to
    // use it (my code was using SwingX Painters, so I’ve not
    // included this.
    // graphics2D.setPaint(yourGradientPaint);

    // fill a rectangle with the gradient paint. notice how we always
    // start from the left edge and fill all the way to the right edge,
    // ignoring the bounds x-values.
    graphics2D.fillRect(0, bounds.y, getWidth(), bounds.height);

    graphics2D.dispose();
    }

    super.paintComponent(g);
    }

  9. Yuriy Says:

    fScrollPane.getViewport().getWidth() is 0 every time in my case. It seems, that scroll pane gets width value after createNodeDimensions() is called.

  10. Ken Says:

    Make sure to call Tree.setLargeModel(trure) as this will cause the node dimensions to get recalculated on every repaint.

  11. Armin St Says:

    Hi!
    Thanks for your comment, but when I override the method createNodeDimensions() then doesn’t actualize the width. The first time paint the panel over the total width, but when i resize the JFrame the size from the panel don’t actualize.

    Thank you!

  12. Ken Says:

    Did you call JTree.setLargeModel(true)?

  13. Armin St Says:

    yes, i set the LargeModel on true …
    Can you send mi a mail, so i send you my code if it is permitted. :)
    Thanks for your help!

  14. karl Says:

    Sorry if this should be obvious, but where is fScrollPane being declared?

    Another potentially stupid question… what’s the preferred way to tell java to use the new implementation of BasicTreeUI?

  15. Ken Says:

    Hi Karl,

    Good point…I should have identified how I got a reference to the JScrollPane.

    Add a listener in your custom TableUI’s installUI method, like this:

    table.addPropertyChangeListener(“ancestor”, fAncestorPropertyChangeListener);

    define fAncestorPropertyChangeListener like this:

    private PropertyChangeListener createAncestorPropertyChangeListener() {
    return new PropertyChangeListener() {
    public void propertyChange(PropertyChangeEvent evt) {
    // indicate that the parent of the JTable has changed.
    parentDidChange();
    }
    };
    }

    and parentDidChange like this:

    private void parentDidChange() {
    // if the parent of the table is an instance of JViewport, and that JViewport’s parent is
    // a JScrollpane, then install the custom BugFixedViewportLayout.
    if (table.getParent() instanceof JViewport
    && table.getParent().getParent() instanceof JScrollPane) {
    fSrollPane = (JScrollPane) table.getParent().getParent();
    fScrollPane.getViewport().setLayout(new BugFixedViewportLayout());
    fScrollPane.getViewport().setUI(new CustomViewportUI());
    fScrollPane.getViewport().setOpaque(false);
    }
    }

    To tell Java to use a new UI delegate, you should really extend (unfortunately) the component like this:

    public void MyJTable extends JTable {
    @Override
    public void updateUI() {
    // install the custom ui delegate.
    setUI(new CustomButtonUI());
    }
    }

    -Ken

  16. karl Says:

    VERY much appreciated Ken. You’ve saved me a huge amount of time. It seems to work right when the nodes are first loaded, but when I resize my component, it does not seem to recalculate the dimensions. Any ideas?

    For anyone else interested, here is my full source code w/ changes:

    import java.awt.Graphics;
    import java.awt.Insets;
    import java.awt.Rectangle;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;

    import javax.swing.JComponent;
    import javax.swing.JScrollPane;
    import javax.swing.JViewport;
    import javax.swing.plaf.basic.BasicTreeUI;
    import javax.swing.tree.AbstractLayoutCache;
    import javax.swing.tree.TreePath;
    import javax.swing.JTree;
    import javax.swing.tree.TreeModel;

    public class ModifiedTreeUI extends BasicTreeUI {

    public ModifiedTreeUI() {
    super();
    }

    private JScrollPane fScrollPane;

    @Override
    public void installUI(JComponent c) {
    super.installUI(c);

    tree.addPropertyChangeListener(“ancestor”, new PropertyChangeListener() {
    public void propertyChange(PropertyChangeEvent evt) {
    parentDidChange();
    }
    });
    }

    private void parentDidChange() {
    if (tree.getParent() instanceof JViewport
    && tree.getParent().getParent() instanceof JScrollPane) {
    fScrollPane = (JScrollPane) tree.getParent().getParent();
    }
    }

    @Override
    protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
    return new NodeDimensionsHandler() {

    @Override
    public Rectangle getNodeDimensions(Object value, int row, int depth, boolean expanded,
    Rectangle size) {

    Rectangle dimensions = super.getNodeDimensions(value, row, depth, expanded, size);
    if (fScrollPane != null) {
    dimensions.width = fScrollPane.getViewport().getWidth() – getRowX(row, depth);
    System.err.println(“width=” + dimensions.width);
    }
    return dimensions;
    }
    };
    }

    @Override
    protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {
    // do nothing.
    }

    @Override
    protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets,
    TreePath path) {
    // do nothing.
    }
    }

    class ModifiedJTree extends JTree {

    public ModifiedJTree(TreeModel model) {
    super(model);
    }

    @Override
    public void updateUI() {
    setUI(new ModifiedTreeUI());
    }
    }

  17. Ken Says:

    Hi Karl,

    A key ingredient to this technique is calling yourJTree.setLargeModel(true). I haven’t traced the intricacies of what this actually does, but it seems to cause the node dimensions to be recalculated rather than cached.

    -Ken

  18. karl Says:

    Hi Ken,

    Thanks, I do have setLargeModel set to true. After adding a println I can see that getNodeDimensions is definitely being called consistently for tree events like expanding/retracting a node. However, it doesn’t seem get fired when the component is resized.

    The following modifications seem to work:

    private void parentDidChange() {
    if (tree.getParent() instanceof JViewport
    && tree.getParent().getParent() instanceof JScrollPane) {
    fScrollPane = (JScrollPane) tree.getParent().getParent();
    fScrollPane.addComponentListener(new ComponentListener() {

    @Override
    public void componentHidden(ComponentEvent e) {

    }

    @Override
    public void componentMoved(ComponentEvent e) {

    }

    @Override
    public void componentResized(ComponentEvent e) {
    configureLayoutCache();
    }

    @Override
    public void componentShown(ComponentEvent e) {

    }

    });
    }
    }

    Not really sure how big of a hit configureLayoutCache() is to performance, but everything seems fine on a smaller model size.

    Karl

  19. Harald K. Says:

    Hi Ken,

    I have a completely different approach to this, and instead create a custom renderer with the following overrides:

    @Override
    public Dimension getPreferredSize() {
    Dimension size = super.getPreferredSize();
    size.width = Short.MAX_VALUE;
    return size;
    }

    @Override
    public void setBounds(final int x, final int y, final int width, final int height) {
    super.setBounds(x, y, Math.min(mTree.getWidth() – x, width), height);
    }

    Gives the same effect, but might be less intrusive than subclassing the UI.

    .k

  20. Harald K. Says:

    Hi again,

    Sorry for replying to an old post.. ;-)

    Do you (or anyone else) have a way of getting the ellipsis (“…”) printed at the end of the TreeCellRenderer? I’m using a DefaultTreeCellRenderer (which extends JLabel) so I would assume it should have worked out of the box. Somehow it doesn’t…

    Thanks,

    .k

  21. Ken Says:

    Hi Harald,

    You’ll find that even the following code using a standard JTree and renderer doesn’t properly show the ellipsis:

    JTree tree = new JTree(new String[]{
    “a value”, “a very very long string”});

    JScrollPane scrollPane = new JScrollPane(tree, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
    JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

    JFrame frame = new JFrame();
    frame.add(scrollPane, BorderLayout.CENTER);
    frame.setSize(200, 200);
    frame.setLocationRelativeTo(null);
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frame.setVisible(true);

    This due to that pesky AbstractLayoutCache I mentioned. My solution address this issue (as well as filling the full tree width), which is part of the reason I settled on it. It’s unfortunate because it requires extending BasicTreeUI which means you can’t simply extend the default look and feel.

    -Ken

  22. Harald K. Says:

    Hi again Ken,

    Using the method I describe above actually fixes the ellipsis problem as well.. I just didn’t use the workaround for that particular case I was trying to solve.. My bad. :-P

    There might be more issues with it though, but I still like this method a lot better than subclassing/replacing the UI.

    Anyways, thanks for the feedback and great work!

    .k

  23. Ken Says:

    Hi Harald,

    I agree with you on sub-classing – if I can avoid it I do! I’ll give your solution a try.

    -Ken

  24. Tommy Says:

    Thanks a lot , Ken.
    You have solved my problem!
    However the childnodes cannot be filled whole and only place after the icon is filled

    Would you like to help me to find why the problem occur??

    import java.awt.Rectangle;

    import javax.swing.JFrame;
    import javax.swing.JTree;
    import javax.swing.plaf.basic.BasicTreeUI;
    import javax.swing.tree.AbstractLayoutCache;
    import javax.swing.tree.DefaultMutableTreeNode;

    public class FilledTree {

    DefaultMutableTreeNode heading = new DefaultMutableTreeNode(” SHOW ALL”);
    JTree database;
    JFrame frame;

    public FilledTree(){
    for(int i=0;i<10;i++){
    DefaultMutableTreeNode temp = new DefaultMutableTreeNode(“No:”+i);
    heading.add(temp);
    }

    database = new JTree(heading);
    database.setUI(new CustomTreeUI());
    database.setLargeModel(true);
    database.setRowHeight(25);

    frame = new JFrame();
    frame.add(database);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
    }

    public static void main(String[]args){
    new FilledTree();
    }

    public class CustomTreeUI extends BasicTreeUI {

    @Override
    protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
    return new NodeDimensionsHandler() {
    @Override
    public Rectangle getNodeDimensions(
    Object value, int row, int depth, boolean expanded,
    Rectangle size) {
    Rectangle dimensions = super.getNodeDimensions(value, row,
    depth, expanded, size);
    dimensions.width = database.getWidth() – getRowX(row, depth);
    return dimensions;
    }
    };
    }
    }
    }

  25. Tommy Says:

    I post the code wrong…..The following one is the right one to show the problem…..

    import java.awt.Color;
    import java.awt.Component;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.Rectangle;
    import java.net.MalformedURLException;
    import java.net.URL;

    import javax.swing.ImageIcon;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JTree;
    import javax.swing.SwingConstants;
    import javax.swing.plaf.basic.BasicTreeUI;
    import javax.swing.tree.AbstractLayoutCache;
    import javax.swing.tree.DefaultMutableTreeNode;
    import javax.swing.tree.TreeCellRenderer;

    public class FilledTree {

    DefaultMutableTreeNode heading = new DefaultMutableTreeNode(” SHOW ALL”);
    JTree database;
    JFrame frame;

    public FilledTree(){
    for(int i=0;i<10;i++){
    DefaultMutableTreeNode temp = new DefaultMutableTreeNode(“No:”+i);
    heading.add(temp);
    }

    database = new JTree(heading);
    database.setCellRenderer(new TreeRenderer());
    database.setUI(new CustomTreeUI());
    database.setLargeModel(true);
    database.setRowHeight(25);

    frame = new JFrame();
    frame.add(database);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
    }

    public static void main(String[]args){
    new FilledTree();
    }

    private class TreeRenderer implements TreeCellRenderer{

    public Component getTreeCellRendererComponent(JTree tree, Object value,
    boolean selected, boolean expanded, boolean leaf, int row,
    boolean hasFocus){

    JLabel label = null;;
    try {
    label = new TextOverImageLabel(new ImageIcon( new URL(“http://forums.sun.com/im/bronze-star.gif”)),” “+value.toString());
    } catch (MalformedURLException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }

    return label;
    }
    }

    private class TextOverImageLabel extends JLabel {

    TextOverImageLabel(ImageIcon icon, String text) {
    super(text, icon, SwingConstants.LEFT); // ignored
    this.setFont(new Font(“Lucida Grande”,Font.BOLD,12));
    this.setBackground(Color.BLACK);
    }

    public void paintComponent(Graphics g) {
    g.drawImage(((ImageIcon)getIcon()).getImage(),0,0,getWidth(),getHeight(),null);
    g.drawString(getText(), 1, 16);
    }
    }

    public class CustomTreeUI extends BasicTreeUI {

    @Override
    protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
    return new NodeDimensionsHandler() {
    @Override
    public Rectangle getNodeDimensions(
    Object value, int row, int depth, boolean expanded,
    Rectangle size) {
    Rectangle dimensions = super.getNodeDimensions(value, row,
    depth, expanded, size);
    dimensions.width = database.getWidth() – getRowX(row, depth);
    return dimensions;
    }
    };
    }
    }
    }

  26. Gmu Says:

    Harald… Thanks dude.. u’r solution works off the bat.

  27. lupurus Says:

    Hi,
    sorry, none of the solutions here works for me. Has anyone a complete example with the solution of Harald?
    And how can I add this gradient?

  28. Matt Says:

    Hi,

    None of the code fragments worked for me. The background of the tree nodes is not filled over the whole tree width. Is there any working solution for Java 1.4 and Default and/or Windows Theme?

    Matt

  29. Jason Says:

    public class StoneTreeUI extends BasicTreeUI {

    @SuppressWarnings(“unused”)
    private static final Logger logger = Logger.getLogger(StoneTreeUI.class);

    public static ComponentUI createUI(JComponent c) {
    return new StoneTreeUI();
    }

    @Override
    protected void installListeners() {
    super.installListeners();

    tree.addComponentListener(componentListener);
    }

    @Override
    protected void uninstallListeners() {
    tree.removeComponentListener(componentListener);

    super.uninstallListeners();
    }

    @Override
    protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
    return new NodeDimensionsHandler() {
    @Override
    public Rectangle getNodeDimensions(Object value, int row, int depth, boolean expanded, Rectangle size) {
    Rectangle dimensions = super.getNodeDimensions(value, row, depth, expanded, size);
    Insets insets = tree.getInsets();
    dimensions.width = tree.getWidth() – getRowX(row, depth) – insets.left – insets.right;
    return dimensions;
    }
    };
    }

    @Override
    protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {
    // do nothing.
    }

    @Override
    protected void paintVerticalLine(Graphics g, JComponent c, int x, int top, int bottom) {
    // do nothing.
    }

    private final ComponentListener componentListener = new ComponentAdapter() {
    @Override
    public void componentResized(ComponentEvent e) {
    treeState.invalidateSizes();
    tree.repaint();
    };
    };

    }

  30. lars Says:

    I have tried your solution and worked through some of the comments here but was not able to create a successful result, there are always some situations where this does not work. For example using drag and drop I experienced that the NodeDimensions were overwritten and this function was not called. I tried something different which uses some more known methods than the proposed ones
    I already use a TreeCellRenderer and added a setPreferredSize(size of JTree’s parent panel) statement, which sets the size for each node. Setting the preferred size works as long as a window resize increases the overall size. If you resize the window to a smaller width the parent panel can not get smaller because the setPreferredSize of the JTree now prevents it :) but there is a solution: I calculate the size in two steps: 1) I calculate the “visible” size of the surrounding component, using a componentListener attached to the JFRame that catches resize events. This Iistener triggers the size calculation of the inner components: in my case I had a JSplitpane with the tree on the right side, so I used “frame width – splitpane divider location – component insets” to calculate the “visible width” of the right component of the SplitPane that contains the JTree. But you have to find your own solution for size calculation which depends on how your tree is embedded.
    2) As a second calculation you have to cover long entries in one of the nodes which may be larger than the calculated visible size. With only the first calculation no srollbars would be enabled. Therefore, I finally calculate the largest width over all tree nodes using string length calculation with fontGraphics once during the initialization of the tree. You need to update this value if a new node is added / removed or if you expand the tree and new nodes become visible. With these calculations you now have two values, use a max function and feed this value into the preferred size … this works fine with all window resize, split pane resize events, drag and drop and also enables scrollers if necessary.

    greetings


Leave a comment