11 Replies Latest reply on Jan 23, 2009 8:36 PM by 843806

    JTree cell width question

    843806
      I'm trying to write a custom TreeCellRenderer that is sensitive to the visible width available to display the value. I can't use tree.getRowBounds(row) from within getTreeCellRendererComponent, because getRowBounds eventually calls getTreeCellRendererComponent again, resulting in stack overflow.

      So how can I get hold of the visible row width (after depth-level indenting) from within getTreeCellRendererComponent?
        • 1. Re: JTree cell width question
          843806
          Do you mean that items which are rendered with your custom TreeCellRenderer is not displayed correctly when you update one of those items? Is the problem that even though the value for one of those items are longer/wider than the previous item, the TreeCell itself seems to not accomodate for the new width (and this displays parts of the new value followed by "...")?

          If that is the case, you need to make sure that the TreeModel you use fire the appropriate events after the update has taken place.
          • 2. Re: JTree cell width question
            843806
            Items are updating fine. My problem is that in my TreeCellRenderer, I can't find a way to adapt the item display to the visible width of the row (which, of course, varies with the depth of the node). I'm not displaying text, but if I were, there would be no trailing ellipsis ("..."); the text would just be clipped at the edge of the JTree. (This is what happens with the TreeTable demo that Sun posted in [an article|http://java.sun.com/products/jfc/tsc/articles/treetable1/].)

            As far as I can tell, the component returned by my TreeCellRenderer is always sized at its preferred width, regardless of how much of it will actually display. That's the crux of my problem.
            • 3. Re: JTree cell width question
              843806
              bump
              • 4. Re: JTree cell width question
                800346
                If we use the following guidelines, there will be no need for any size adjustment in the renderer:
                If we changed the text of a node, we call nodeChanged. The rest takes care of itself automatically. In order to insert a node, we call either directly insertNodeInto (and the model will insert the child node under the parent node), or we call nodesWereInserted, after the node was inserted. Hope this helps.
                • 5. Re: JTree cell width question
                  843806
                  This is not helpful.

                  Maybe I'm not being clear. I'm having no problem with the JTree updating when the contents of nodes change. This is not a problem with data changing, the tree structure changing, or anything like that.

                  I'm having problems trying to render my data because the way a node should be rendered depends on how much real estate is available to render it. If I ask the JTree how much width is available visually for a particular row or TreePath, it calls on my renderer. So if I ask the JTree for this information from within the renderer, everything goes boom. That's the core of the problem.

                  I'll repeat that I need a custom renderer because I'm not rendering my data as text; my renderer will return a custom JComponent that is not a subclass of JLabel. JTree's default renderer is useless for this purpose.
                  • 6. Re: JTree cell width question
                    darrylburke
                    To get better help sooner, post a SSCCE that clearly demonstrates your problem.

                    Use code tags to post codes -- [code]CODE[/code] will display as
                    CODE
                    Or click the CODE button and paste your code between the {code} tags that appear.

                    db
                    • 7. Re: JTree cell width question
                      843806
                      The comment out code causes a stack overflow. What I'm trying to do is set the preferred width of the custom component to the visible width available for displaying values on the screen. In this example, the purpose is to right-align the second of two words.

                      Unfortunately, I think this approach is doomed anyway. As far as I can tell, JTree provides no way to query how much space will be visible on a row; getRowBounds() returns the actual size of the row component, and JTree also lays out row components at their preferred size. Sigh.
                      import javax.swing.*;
                      import javax.swing.tree.*;
                      import java.awt.*;
                      import java.util.*;
                      
                      class MyRenderer implements TreeCellRenderer, SwingConstants {
                           /**
                            * If the value is an instance of WordPair, the (currently
                            * commented out) code tries to set the preferred size of
                            * the returned component so that the second word of the
                            * WordPair is right-aligned on the tree's visible width.
                            */
                           public Component getTreeCellRendererComponent(JTree tree, Object value,
                                     boolean selected, boolean expanded, boolean leaf, int row,
                                     boolean hasFocus) {
                                Component rv = null;
                                if (value instanceof DefaultMutableTreeNode) {
                                     Object obj = ((DefaultMutableTreeNode) value).getUserObject();
                                     if (obj instanceof WordPair) {
                                          renderer.words = (WordPair) obj;
                                          if (selected) {
                                               renderer.setForeground(fbr.getTextSelectionColor());
                                               renderer.setBackground(fbr.getBackgroundSelectionColor());
                                          } else {
                                               renderer.setForeground(fbr.getTextNonSelectionColor());
                                               renderer.setBackground(fbr.getBackgroundNonSelectionColor());
                                          }
                                          renderer.setEnabled(tree.isEnabled());
                                          rv = renderer;
                                          Dimension size = renderer.getMinimumSize();
                      /* [BAD CODE]
                                          // Next line CAUSES STACK OVERFLOW for root-level WordPair nodes
                                          Rectangle rect = tree.getRowBounds(row);
                                          if (rect != null) size.width = Math.max(size.width, rect.width);
                      // [/BAD_CODE] */
                                          renderer.setPreferredSize(size);
                                     }
                                }
                                if (rv == null) {
                                     rv = fbr.getTreeCellRendererComponent(tree,
                                           value, selected, expanded, leaf, row, hasFocus);
                                }
                                return rv;
                           }
                      
                           MyRenderer() {
                                fbr = new DefaultTreeCellRenderer();
                                renderer.setOpaque(true);
                                renderer.setFont(UIManager.getFont("Tree.font"));
                           }
                      
                           /** The {@code WordPair} renderer. */
                           private WordPairView renderer = new WordPairView();
                           /** The fall-back renderer for non-{@code WordPair} values. */
                           private DefaultTreeCellRenderer fbr;
                      }
                      
                      class WordPair {
                           WordPair(String word1, String word2) {
                                this.word1 = word1;
                                this.word2 = word2;
                           }
                           String word1;
                           String word2;
                      }
                      
                      class WordPairView extends JComponent {
                           WordPair words;
                      
                           public void paintComponent(Graphics g) {
                                super.paintComponent(g);
                                if (words != null) {
                                     Insets in = getInsets();
                                     FontMetrics fm = getFontMetrics(g.getFont());
                                     int x = in.left;
                                     int y = in.top + fm.getAscent();
                                     g.drawString(words.word1, x, y);
                                     x = getWidth() - in.right - fm.stringWidth(words.word2);
                                     g.drawString(words.word2, x, y);
                                }
                           }
                      
                           public Dimension getMinimumSize() {
                                FontMetrics fm = getFontMetrics(getFont());
                                Insets in = getInsets();
                                Dimension rv = new Dimension(in.left + in.right + 10, in.top + in.bottom + fm.getHeight());
                                if (words != null) {
                                     rv.width += fm.stringWidth(words.word1) + fm.stringWidth(words.word2);
                                }
                                return rv;
                           }
                      }
                      
                      class NamedVector extends Vector<Object> {
                           String name;
                      
                           NamedVector(String name) {
                                this.name = name;
                           }
                      
                           NamedVector(String name, Object [] elements) {
                                this.name = name;
                                for (Object t : elements) {
                                      add(t);
                                }
                           }
                      
                           public String toString() {
                                return "[" + name + "]";
                           }
                      }
                      
                      public class JTreeTest extends JFrame {
                           DefaultTreeModel treeModel;
                           JTreeTest() {
                                super("JTree Renderer Test");
                                setDefaultCloseOperation(EXIT_ON_CLOSE);
                                JTree tree = new JTree(createNodes()) {
                      
                                     /**
                                      * Clamp width of tree to width of scroller.
                                      */
                                     public boolean getScrollableTracksViewportWidth() {
                                          return true;
                                     }
                                };
                                tree.setCellRenderer(new MyRenderer());
                                treeModel = (DefaultTreeModel) tree.getModel();
                                JScrollPane treeView = new JScrollPane(tree);
                                add(treeView, BorderLayout.CENTER);
                                pack();
                           }
                      
                           Vector createNodes() {
                                Object [] nouns = {
                                     new WordPair("fork", "tenedor"),
                                };
                                Object [] verbs = {
                                     new WordPair("lift", "levantar"),
                                };
                                WordPair soloItem = new WordPair("demo", "demo");
                                NamedVector nounsVec = new NamedVector("Nouns", nouns);
                                NamedVector verbsVec = new NamedVector("Verbs", verbs);
                                return new NamedVector("Root", new Object[]{soloItem, nounsVec, verbsVec});
                           }
                      
                           public static void main(String[] args) {
                                EventQueue.invokeLater(new Runnable() {
                      
                                     public void run() {
                                          JTreeTest jt = new JTreeTest();
                                          jt.setVisible(true);
                                     }
                                });
                           }
                      }
                      Ted Hopp
                      • 8. Re: JTree cell width question
                        darrylburke
                        This is my attempt. I've messed up somewhere so the word-pair nodes aren't getting selected, but I have other stuff to do and can't take the time to rectify it.

                        Rendering is as you wanted.
                        import java.awt.BorderLayout;
                        import java.awt.Component;
                        import java.awt.Dimension;
                        import java.awt.EventQueue;
                        import java.awt.FontMetrics;
                        import java.awt.Graphics;
                        import java.util.Vector;
                        import javax.swing.JComponent;
                        import javax.swing.JFrame;
                        import javax.swing.JScrollPane;
                        import javax.swing.JTree;
                        import javax.swing.tree.DefaultMutableTreeNode;
                        import javax.swing.tree.DefaultTreeCellRenderer;
                        
                        public class JTreeTest extends JFrame {
                        
                           JTree tree;
                        
                           JTreeTest() {
                              super("JTree Renderer Test");
                              setDefaultCloseOperation(EXIT_ON_CLOSE);
                              tree = new JTree(createNodes()) {
                        
                                 @Override
                                 public boolean getScrollableTracksViewportWidth() {
                                    return true;
                                 }
                              };
                              tree.setCellRenderer(new MyRenderer());
                              JScrollPane treeView = new JScrollPane(tree);
                              add(treeView, BorderLayout.CENTER);
                              setSize(400, 400);
                           }
                        
                           public static void main(String[] args) {
                              EventQueue.invokeLater(new Runnable() {
                        
                                 public void run() {
                                    new JTreeTest().setVisible(true);
                                 }
                              });
                           }
                        
                           Vector createNodes() {
                              Object[] nouns = {
                                 new WordPair("fork", "tenedor"),
                              };
                              Object[] verbs = {
                                 new WordPair("lift", "levantar"),
                              };
                              WordPair soloItem = new WordPair("demo", "demo");
                              NamedVector nounsVec = new NamedVector("Nouns", nouns);
                              NamedVector verbsVec = new NamedVector("Verbs", verbs);
                              return new NamedVector("Root", new Object[]{soloItem, nounsVec, verbsVec});
                           }
                        }
                        
                        class NamedVector extends Vector<Object> {
                        
                           String name;
                        
                           NamedVector(String name) {
                              this.name = name;
                           }
                        
                           NamedVector(String name, Object[] elements) {
                              this.name = name;
                              for (Object t : elements) {
                                 add(t);
                              }
                           }
                        
                           @Override
                           public String toString() {
                              return "[" + name + "]";
                           }
                        }
                        
                        class MyRenderer extends DefaultTreeCellRenderer {
                        
                           @Override
                           public Component getTreeCellRendererComponent(JTree tree, Object value,
                                   boolean selected, boolean expanded, boolean leaf, int row,
                                   boolean hasFocus) {
                              super.getTreeCellRendererComponent(tree, value, selected, expanded,
                                      leaf, row, hasFocus);
                              Component retVal = this;
                              if (value instanceof DefaultMutableTreeNode) {
                                 Object obj = ((DefaultMutableTreeNode) value).getUserObject();
                                 if (obj instanceof WordPair) {
                                    int ht = getPreferredSize().height;
                                    retVal = new WordPairView((WordPair) obj, tree);
                                    retVal.setPreferredSize(new Dimension(Integer.MAX_VALUE, ht));
                                 }
                              }
                        
                              return retVal;
                           }
                        }
                        
                        class WordPair {
                        
                           String word1;
                           String word2;
                        
                           WordPair(String word1, String word2) {
                              this.word1 = word1;
                              this.word2 = word2;
                           }
                        }
                        
                        class WordPairView extends JComponent {
                        
                           WordPair words;
                           JTree tree;
                           final static int margin = 5;
                        
                           public WordPairView(WordPair words, JTree tree) {
                              this.words = words;
                              this.tree = tree;
                              setOpaque(true);
                           }
                        
                           @Override
                           public void paintComponent(Graphics g) {
                              super.paintComponent(g);
                              FontMetrics fm = getFontMetrics(g.getFont());
                              int x = margin;
                              int y = margin + fm.getAscent();
                              g.drawString(words.word1, x, y);
                              x = tree.getWidth() - margin - fm.stringWidth(words.word2) - getX();
                              x = Math.max(x, 2 * margin + fm.stringWidth(words.word1));
                              g.drawString(words.word2, x, y);
                           }
                        }
                        db
                        • 9. Re: JTree cell width question
                          843806
                          Thanks! That helps a lot.

                          Ted
                          • 10. Re: JTree cell width question
                            darrylburke
                            OK. The custom nodes weren't getting selected because of the overkill preferred width of Integer.MAX_VALUE. Couldn't trace out exactly where, but at some point of computation internal to the Swing classes, that was being incremented by the getX() of the node and overflowing into the negative.

                            This is a completely working and simplified implementation, which does away with the WordPairView class in favor of using the renderer itself as a View.
                            import java.awt.Component;
                            import java.awt.Dimension;
                            import java.awt.FontMetrics;
                            import java.awt.Graphics;
                            import java.awt.Toolkit;
                            import java.util.Vector;
                            import javax.swing.JFrame;
                            import javax.swing.JScrollPane;
                            import javax.swing.JTree;
                            import javax.swing.SwingUtilities;
                            import javax.swing.tree.DefaultMutableTreeNode;
                            import javax.swing.tree.DefaultTreeCellRenderer;
                            
                            public class WordPairTreeCellRendererTest {
                            
                               public static void main(String[] args) {
                                  SwingUtilities.invokeLater(new Runnable() {
                            
                                     @Override
                                     public void run() {
                                        new WordPairTreeCellRendererTest().makeUI();
                                     }
                                  });
                               }
                            
                               public void makeUI() {
                                  JTree tree = new JTree(createNodes()) {
                            
                                     @Override
                                     public boolean getScrollableTracksViewportWidth() {
                                        return true;
                                     }
                                  };
                                  tree.setCellRenderer(new WordPairTreeCellRenderer(tree));
                                  JScrollPane scrollPane = new JScrollPane(tree);
                            
                                  JFrame frame = new JFrame("JTree Renderer Test");
                                  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                                  frame.add(scrollPane);
                                  frame.setSize(400, 400);
                                  frame.setLocationRelativeTo(null);
                                  frame.setVisible(true);
                               }
                            
                               Vector createNodes() {
                                  WordPair soloItem = new WordPair("demo", "demo");
                                  Object[] nouns = {new WordPair("fork", "tenedor")};
                                  NamedVector nounsVec = new NamedVector("Nouns", nouns);
                                  Object[] verbs = {new WordPair("lift", "levantar")};
                                  NamedVector verbsVec = new NamedVector("Verbs", verbs);
                            
                                  return new NamedVector("Root", new Object[]{soloItem, nounsVec, verbsVec});
                               }
                            }
                            
                            class NamedVector extends Vector<Object> {
                            
                               private final String name;
                            
                               NamedVector(String name, Object[] elements) {
                                  this.name = name;
                                  for (Object t : elements) {
                                     add(t);
                                  }
                               }
                            
                               @Override
                               public String toString() {
                                  return "[" + name + "]";
                               }
                            }
                            
                            class WordPair {
                            
                               String word1;
                               String word2;
                            
                               WordPair(String word1, String word2) {
                                  this.word1 = word1;
                                  this.word2 = word2;
                               }
                            }
                            
                            class WordPairTreeCellRenderer extends DefaultTreeCellRenderer {
                            
                               private final JTree tree;
                               private final int screenWidth;
                               private Object userObject;
                            
                               public WordPairTreeCellRenderer(JTree tree) {
                                  this.tree = tree;
                                  screenWidth = Toolkit.getDefaultToolkit().getScreenSize().width;
                               }
                            
                               @Override
                               public Component getTreeCellRendererComponent(JTree tree, Object value,
                                     boolean selected, boolean expanded, boolean leaf, int row,
                                     boolean hasFocus) {
                                  super.getTreeCellRendererComponent(tree, value, selected, expanded,
                                        leaf, row, hasFocus);
                                  userObject = ((DefaultMutableTreeNode) value).getUserObject();
                                  return this;
                               }
                            
                               @Override
                               public void paintComponent(Graphics g) {
                                  if (userObject instanceof WordPair) {
                                     WordPair pair = (WordPair) userObject;
                                     setText("");
                                     super.paintComponent(g);
                            
                                     FontMetrics metrics = getFontMetrics(g.getFont());
                                     int y = (getIcon().getIconHeight() + metrics.getAscent()) / 2;
                                     int gap = getIconTextGap();
                            
                                     int word1x = getIcon().getIconWidth() + gap;
                                     g.drawString(pair.word1, word1x, y);
                            
                                     int word2x = tree.getWidth() - getX() -
                                           metrics.stringWidth(pair.word2) - gap;
                                     word2x = Math.max(word2x,
                                           word1x + metrics.stringWidth(pair.word1) + gap);
                                     g.drawString(pair.word2, word2x, y);
                                  } else {
                                     super.paintComponent(g);
                                  }
                               }
                            
                               @Override
                               public Dimension getPreferredSize() {
                                  Dimension d = super.getPreferredSize();
                                  if (userObject instanceof WordPair) {
                                     d.width = screenWidth;
                                  }
                                  return d;
                               }
                            }
                            db

                            edit Made a few minor changes and removed an unused constructor.

                            Edited by: Darryl.Burke
                            • 11. Re: JTree cell width question
                              843806
                              Thanks for all the effort. Unfortunately, this approach doesn't quite do the trick. The problem is that all decisions about size are deferred to painting. This doesn't work if the preferred height of the component depends on the width (think of a line-wrapped node). Waiting until painting to work it out is too late for this.

                              I realize that the returned component may be asked for its preferred size before there is a known width for the tree, but the same problem is resolved by JTextArea (but not in a way usable in a JTree). But once the tree width is determined and the tree wants a cell renderer, I'd like to return one with a height that may vary with the width that will actually be visible. I just can't figure out how to do this.

                              I did try the following for a rendering component:
                                  @Override
                                  public Dimension getPreferredSize() {
                                      TreePath path = tree.getPathForRow(row); // tree and row are set in c'tor
                                      TreeUI ui_ = tree.getUI();
                                      Insets in = tree.getInsets();
                                      int w = tree.getWidth() - in.left - in.right;
                                      if (path != null && ui_ instanceof BasicTreeUI) {
                                          BasicTreeUI treeUI = (BasicTreeUI) ui_;
                                          int depth = path.getPathCount() - 1;
                                          int depthAdjust = 0;
                                          if (tree.isRootVisible()) {
                                              if (tree.getShowsRootHandles()) {
                                                  depthAdjust = 1;
                                              }
                                          } else if (!tree.getShowsRootHandles()) {
                                              depthAdjust = -1;
                                          }
                                          int totalIndent = treeUI.getLeftChildIndent() + treeUI.getRightChildIndent();
                                          w -= totalIndent * (depth + depthAdjust);
                                      }
                                      Dimension size = getMinimumSize();
                                      size.width = Math.max(size.width, w);
                                      return size;
                                  }
                              Aside from the fact that this is limited to Basic and Metal plafs and it doesn't work (it seems to overestimate the available width), it also appears that the value is getting cached somewhere and the cached value is used when the tree changes width.