How to fully expand a large JTree efficiently - java

There are several related questions, about auto-expanding a JTree when a new TreeModel is set, or about expanding a JTree in general, and some of them are also aiming at the performance of expanding many paths in a JTree.
However, none of the proposed solutions seems to cover what one could consider a "simple" application case: I have a large tree (that is, a tree that is either very deep, very broad, or both), and I want to fully expand it programmatically.
The following is a MCVE that shows the problem: It creates a tree model with 100k nodes. Pressing the button triggers a call to expandAll, which tries to expand all nodes using an approach that was derived from the answers to the related questions.
The problem is that expanding these 100k nodes takes approximately 13 seconds (on an average machine, with a recent JVM).
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.util.function.Consumer;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
public class TreeExpansionPerformanceProblem
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(
() -> createAndShowGUI());
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().setLayout(new GridLayout(1,0));
f.getContentPane().add(createTestPanel(
TreeExpansionPerformanceProblem::expandAll));
f.setSize(800,600);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
{
JPanel panel = new JPanel(new BorderLayout());
JTree tree = new JTree(createTestTreeModel());
panel.add(new JScrollPane(tree), BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
JButton expandAllButton = new JButton("Expand all");
expandAllButton.addActionListener( e ->
{
System.out.println("Expanding...");
long before = System.nanoTime();
expansionMethod.accept(tree);
long after = System.nanoTime();
System.out.println("Expanding took "+(after-before)/1e6);
});
buttonPanel.add(expandAllButton);
panel.add(buttonPanel, BorderLayout.SOUTH);
return panel;
}
private static void expandAll(JTree tree)
{
int r = 0;
while (r < tree.getRowCount())
{
tree.expandRow(r);
r++;
}
}
private static TreeModel createTestTreeModel()
{
DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
addNodes(root, 0, 6, 6, 10);
return new DefaultTreeModel(root);
}
private static void addNodes(DefaultMutableTreeNode node,
int depth, int maxDepth, int count, int leafCount)
{
if (depth == maxDepth)
{
return;
}
for (int i=0; i<leafCount; i++)
{
DefaultMutableTreeNode leaf =
new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
node.add(leaf);
}
if (depth < maxDepth - 1)
{
for (int i=0; i<count; i++)
{
DefaultMutableTreeNode child =
new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
node.add(child);
addNodes(child, depth+1, maxDepth, count, leafCount);
}
}
}
}
Are there any options that allow expanding many nodes more efficiently?

There are various bottlenecks when fully expanding a large tree, and different ways to circumvent these.
Interestingly, collecting the TreePath objects for the expansion and traversing the tree in general is not the most expensive part. According to profiler runs in the VisualVM and in the Java Flight Recorder, most of the time is spent when computing the "mapping" between the model state (the TreeModel) and the view (the JTree). This mainly refers to
computing the row heights for the JTree
computing the bounds of the labels in the TreeCellRenderer
Not all of these computations may be avoided. However, expanding the tree can be made significantly faster by
setting a fixed row height, with JTree#setRowHeight
temporarily disabling the TreeExpansionListeners of the tree
The following is an MCVE that compares the "naïve" approach from the question, which takes 13 seconds for expanding a tree with 100k nodes, to a slightly faster approach, that only takes 1 second for expanding the same tree.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
public class TreeExpansionPerformanceSolution
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(
() -> createAndShowGUI());
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().setLayout(new GridLayout(1,0));
f.getContentPane().add(createTestPanel(
TreeExpansionPerformanceSolution::expandAll));
f.getContentPane().add(createTestPanel(
TreeExpansionPerformanceSolution::expandAllFaster));
f.setSize(800,600);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
{
JPanel panel = new JPanel(new BorderLayout());
JTree tree = new JTree(createTestTreeModel());
panel.add(new JScrollPane(tree), BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
JButton expandAllButton = new JButton("Expand all");
expandAllButton.addActionListener( e ->
{
System.out.println("Expanding...");
long before = System.nanoTime();
expansionMethod.accept(tree);
long after = System.nanoTime();
System.out.println("Expanding took "+(after-before)/1e6);
});
buttonPanel.add(expandAllButton);
panel.add(buttonPanel, BorderLayout.SOUTH);
return panel;
}
private static void expandAll(JTree tree)
{
int r = 0;
while (r < tree.getRowCount())
{
tree.expandRow(r);
r++;
}
}
private static void expandAllFaster(JTree tree)
{
// Determine a suitable row height for the tree, based on the
// size of the component that is used for rendering the root
TreeCellRenderer cellRenderer = tree.getCellRenderer();
Component treeCellRendererComponent =
cellRenderer.getTreeCellRendererComponent(
tree, tree.getModel().getRoot(), false, false, false, 1, false);
int rowHeight = treeCellRendererComponent.getPreferredSize().height + 2;
tree.setRowHeight(rowHeight);
// Temporarily remove all listeners that would otherwise
// be flooded with TreeExpansionEvents
List<TreeExpansionListener> expansionListeners =
Arrays.asList(tree.getTreeExpansionListeners());
for (TreeExpansionListener expansionListener : expansionListeners)
{
tree.removeTreeExpansionListener(expansionListener);
}
// Recursively expand all nodes of the tree
TreePath rootPath = new TreePath(tree.getModel().getRoot());
expandAllRecursively(tree, rootPath);
// Restore the listeners that the tree originally had
for (TreeExpansionListener expansionListener : expansionListeners)
{
tree.addTreeExpansionListener(expansionListener);
}
// Trigger an update for the TreeExpansionListeners
tree.collapsePath(rootPath);
tree.expandPath(rootPath);
}
// Recursively expand the given path and its child paths in the given tree
private static void expandAllRecursively(JTree tree, TreePath treePath)
{
TreeModel model = tree.getModel();
Object lastPathComponent = treePath.getLastPathComponent();
int childCount = model.getChildCount(lastPathComponent);
if (childCount == 0)
{
return;
}
tree.expandPath(treePath);
for (int i=0; i<childCount; i++)
{
Object child = model.getChild(lastPathComponent, i);
int grandChildCount = model.getChildCount(child);
if (grandChildCount > 0)
{
class LocalTreePath extends TreePath
{
private static final long serialVersionUID = 0;
public LocalTreePath(
TreePath parent, Object lastPathComponent)
{
super(parent, lastPathComponent);
}
}
TreePath nextTreePath = new LocalTreePath(treePath, child);
expandAllRecursively(tree, nextTreePath);
}
}
}
private static TreeModel createTestTreeModel()
{
DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
addNodes(root, 0, 6, 6, 10);
return new DefaultTreeModel(root);
}
private static void addNodes(DefaultMutableTreeNode node,
int depth, int maxDepth, int count, int leafCount)
{
if (depth == maxDepth)
{
return;
}
for (int i=0; i<leafCount; i++)
{
DefaultMutableTreeNode leaf =
new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
node.add(leaf);
}
if (depth < maxDepth - 1)
{
for (int i=0; i<count; i++)
{
DefaultMutableTreeNode child =
new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
node.add(child);
addNodes(child, depth+1, maxDepth, count, leafCount);
}
}
}
}
Notes:
This is a self-answered question, and I hope that this answer may be helpful for others. Nevertheless, 1 second is still rather slow. I tried some other things as well, e.g. setting tree.setLargeModel(true), but this did not have a positive effect (in fact, it was even slower!). Most of the time is still spent in the final update of the visual state of the tree, and I'd be happy to see further improvements here.
The expandAllRecursively method could be replaced by few lines involving DefaultMutableTreeNode#breadthFirstEnumeration and DefaultTreeModel#getPathToRoot, without sacrificing much of the performance. But in the current form, the code solely operates on the TreeModel interface, and should work with any kind of nodes.

As discussed here, JTree already uses the flyweight pattern to optimize rendering. I'd argue that your approach in expandAllFaster() is sufficient. Expanding all of >105 leaves is unwieldy at best. The resulting tree is difficult to browse meaningfully, although suitable search controls may mitigate this.
An interesting compromise is seen in the Mac OS X TreeUI delegate, com.apple.laf.AquaTreeUI. It recursively expands the selected node and its children when the option key is pressed, as determined by MouseEvent::isAltDown(). See the Action named "aquaFullyExpandNode" for details.
Finally, saving the user's expansion as a preference might be worthwhile, for example.
I'm working on…filtering a >100k-node-JTree on the fly.
Focusing on a model-based approach, as suggested here, move the search to a separate, perhaps modeless, dialog. In outline,
Construct a prefix tree based on the tree model to be used as a dictionary, perhaps using one of the approaches suggested here.
Let a DocumentListener monitor the search field and condition a custom TableModel to display matches as the user types.
Display no matches until some minimum number of characters has been typed; three is a common choice for large models.
Let a TableModelListener expand tree nodes corresponding to selected rows; alternatively, expand selected rows in an Expand button handler; in a modeless context, fire a suitable PropertyChangeEvent for which the tree should listen.

Related

more efficiently load images for JList, presumably with SwingWorker

I have a problem in which I hope SwingWorker can help me, but I am not quite sure how to integrate it in my program.
The problem:
In a CardLayout I have a button on Card1 that opens Card2.
Card2 has a JList with a custom renderer(extending JLabel) which will display on average 1 to 6 images which are:
PNGs
around 500kb in size
loaded via imageIO with the change of cards
the renderer applies heavy operations such as image scaling or blurring and than sets the image as JLabel icon.
This can almost take up to a second if around 6 images have to be rendered, which is does not happen frequently but even that occasional split second of unresponsiveness feels bad.
Now I thought a SwingWorker might help here, but I'm thoroughly confused as to how I would have to integrate it.
Assuming we had this Code snippet
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class Example {
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList();
private JScrollPane scroll = new JScrollPane();
private Map<String, Color> colorMap = new HashMap<>();
public Example() {
colorMap.put("red", Color.red);
colorMap.put("blue", Color.blue);
colorMap.put("cyan", Color.cyan);
colorMap.put("green", Color.green);
colorMap.put("yellow", Color.yellow);
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80,200));
list.setCellRenderer(new CustomRenderer());
DefaultListModel model = new DefaultListModel();
model.addElement(new Product("red"));
model.addElement(new Product("yellow"));
model.addElement(new Product("blue"));
model.addElement(new Product("red"));
model.addElement(new Product("cyan"));
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(new Example().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
class CustomRenderer extends JLabel implements ListCellRenderer<Product> {
private Product product;
public CustomRenderer() {
setOpaque(false);
}
#Override
public Component getListCellRendererComponent(JList<? extends Product> list, Product product, int index, boolean isSelected, boolean cellHasFocus) {
this.product = product;
/**
* in the actual code image is png with alpha channel respectively named to the productID of the JList object
*
* String id = product.getId();
* image = ImageIO.read(getClass().getResource("../../resources/images/" + id + ".png"));
*/
BufferedImage image1 = new BufferedImage(80, 50, BufferedImage.TYPE_INT_RGB);
BufferedImage image2 = new BufferedImage( 80, 75, BufferedImage.TYPE_INT_RGB);
Graphics g = image2.getGraphics();
/**
* this is only an example, in the actual code I might also apply gaussian blurs or rescale several time
*/
g.drawImage(image1,0,0,null);
setIcon(new ImageIcon(image2));
return this;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(colorMap.get(product.getColor()));
g.fillRect(0,0,80,75);
}
}
class Product {
String productID;
String color;
public Product(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public String getProductID() {
return productID;
}
}
}
would I have to call a SwingWorker from every getListCellRendererComponent call
to take over the image operations ?
Is SwingWorker even the right tool for this problem?
any help as to how I can make this part of my GUI faster would be greatly appreciated.
EDIT:
Hovercraft Full Of Eels mentioned that preloading the images could help and that loading the images from the renderer is fundamentally wrong.
This leads me to another Question:
I have a list(let's call it list1) with around 3000 objects each object has a 8kb jpg thumbnail which is load via object ID (also during the rendering)
The list displays around 6 to 12 of these thumbnail at the same time (due to the List's Dimension)
when the user selects an object he can press a button to display Card2 from the Cardlayout mentioned in the original question and it's list(list2) with the Object
and all it's related Object in non thumbnail view (500kb png + heavy image operation). Now I think it would be feasible to preload the non thumbnail image of the Object and it's relations selected in the first list which would be around 1-6 images. If I understood correctly what Hovercraft Full Of Eels said, then I could use a SwingWorker to load these Images after the selection of an Object from list1.
But what about the around 3000 images from list1, the program seemingly is not slowed down or becomes unresponsive because they are rather small in size and there are no heavy operations on the thumbnails, but they are still load form the list1's renderer. Would it make sense to preload the several thousand thumbnails ?
btw. feel free to tell me if this kind of question edit is not wished for and if it should be made into a question of itself.
One approach might be the following:
Whenever a cell renderer component for a certain element (Product) is requested, you check whether the matching image is already loaded. If not, you start a Swing worker that does the work of loading and processing the image in the background. When the worker is done, the image is placed into a cache for later lookup. In the meantime, you let the renderer just say "Loading..." or something.
A very quick implementation is here:
And as an MCVE:
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.SwingWorker;
public class LazyImageLoadingCellRendererTest
{
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList<Product>();
private JScrollPane scroll = new JScrollPane();
public LazyImageLoadingCellRendererTest()
{
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80, 200));
list.setCellRenderer(new LazyImageLoadingCellRenderer<Product>(list,
LazyImageLoadingCellRendererTest::loadAndProcessImage));
DefaultListModel<Product> model = new DefaultListModel<Product>();
for (int i=0; i<1000; i++)
{
model.addElement(new Product("id" + i));
}
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException
{
EventQueue.invokeLater(new Runnable()
{
#Override
public void run()
{
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(
new LazyImageLoadingCellRendererTest().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
private static final Random random = new Random(0);
private static BufferedImage loadAndProcessImage(Product product)
{
String id = product.getProductID();
int w = 100;
int h = 20;
BufferedImage image =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.GREEN);
g.fillRect(0, 0, w, h);
g.setColor(Color.BLACK);
g.drawString(id, 10, 16);
g.dispose();
long delay = 500 + random.nextInt(3000);
try
{
System.out.println("Load time of " + delay + " ms for " + id);
Thread.sleep(delay);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return image;
}
class Product
{
String productID;
public Product(String productID)
{
this.productID = productID;
}
public String getProductID()
{
return productID;
}
}
}
class LazyImageLoadingCellRenderer<T> extends JLabel
implements ListCellRenderer<T>
{
private final JList<?> owner;
private final Function<? super T, ? extends BufferedImage> imageLookup;
private final Set<T> pendingImages;
private final Map<T, BufferedImage> loadedImages;
public LazyImageLoadingCellRenderer(JList<?> owner,
Function<? super T, ? extends BufferedImage> imageLookup)
{
this.owner = Objects.requireNonNull(
owner, "The owner may not be null");
this.imageLookup = Objects.requireNonNull(imageLookup,
"The imageLookup may not be null");
this.loadedImages = new ConcurrentHashMap<T, BufferedImage>();
this.pendingImages =
Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>());
setOpaque(false);
}
class ImageLoadingWorker extends SwingWorker<BufferedImage, Void>
{
private final T element;
ImageLoadingWorker(T element)
{
this.element = element;
pendingImages.add(element);
}
#Override
protected BufferedImage doInBackground() throws Exception
{
try
{
BufferedImage image = imageLookup.apply(element);
loadedImages.put(element, image);
pendingImages.remove(element);
return image;
}
catch (Exception e)
{
e.printStackTrace();
return null;
}
}
#Override
protected void done()
{
owner.repaint();
}
}
#Override
public Component getListCellRendererComponent(JList<? extends T> list,
T value, int index, boolean isSelected, boolean cellHasFocus)
{
BufferedImage image = loadedImages.get(value);
if (image == null)
{
if (!pendingImages.contains(value))
{
//System.out.println("Execute for " + value);
ImageLoadingWorker worker = new ImageLoadingWorker(value);
worker.execute();
}
setText("Loading...");
setIcon(null);
}
else
{
setText(null);
setIcon(new ImageIcon(image));
}
return this;
}
}
Note:
This is really just a quick example showing the general approach. Of course, this could be improved in many ways. Although the actual loading process is already pulled out into a Function (thus making it generically applicable for any sort of image, regardless of where it comes from), one major caveat is that: It will try to load all images. A nice extension would be to add some smartness here, and make sure that it only loads the images for which the cells are currently visible. For example, when you have a list of 1000 elements, and want to see the last 10 elements, then you should not have to wait for 990 elements to be loaded. The last elements should be priorized higher and loaded first. However, for this, a slightly larger infrastructure (mainly: an own task queue and some stronger connection to the list and its scroll pane) may be necessary. (I'll possibly tackle this one day, because it might be a nice and interesting thing to have, but until then, the example above might do it...)
would I have to call a SwingWorker from every getListCellRendererComponent call to take over the image operations ?
No, you would in fact never call a background thread from within a key rendering method. In fact this appears to be the main problem with the code above -- you're reading in images from within a rendering method, significantly reducing the perceived responsiveness of your program.
Is SwingWorker even the right tool for this problem?
Perhaps, but not where you're thinking about using it. A SwingWorker will not speed anything up, but by performing long-running tasks in the background, it would avoid blocking the Swing event thread, freezing the GUI. Best would be to read the images, once, perhaps in a SwingWorker if not done during program initiation, and save them to a variable. Do not re-read the image every time you want to render it, if this can be avoided. And again, do not read in the image from within your rendering code as this will significantly reduce the perceived responsiveness of the program.

JTree is badly blurred when scrolling [closed]

It's difficult to tell what is being asked here. This question is ambiguous, vague, incomplete, overly broad, or rhetorical and cannot be reasonably answered in its current form. For help clarifying this question so that it can be reopened, visit the help center.
Closed 9 years ago.
I have a JTree inside a JScrollPane, and when I use the scrollbar the tree gets all blurred up, as you can see in the image below.
It gets back to normal if I do something to make it repaint, like minimize and restore the window, or click in the tree to make a node expand or collapse (however the blurring doesn't go away if I drag the window off the screen and back, or drag another window in front of it).
The JTree has a custom TreeModel and cell renderer. The recent change was for the TreeModel; the cell renderer has been there for a long time and was working fine. The cell renderer is a subclass of DefaultTreeCellRenderer, with only the getTreeCellRendererComponent method overridden (to display custom icons).
I used to populate DefaultMutableTreeNodes from a data structure which contained the data to be displayed, but that was giving performance problems when the number of nodes was large (like over 10,000). Since the data I had was already in a tree structure, I realized it would be fairly simple to create a custom TreeModel around it without using any DefaultMutableTreeNodes. That made the JTree populate more quickly, but now I'm left with this blurred scrolling problem.
The code below isn't from the application, but it compiles as is and will demonstrate the problem. Removing the tree.setBackground line stops the blurry behavior.
package stackoverflow;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.*;
import java.awt.Color;
public class NumberTreeModel implements TreeModel {
public static final int ROOT_NUMBER = 100;
public Object getChild(Object parent, int index) {
return index;
}
public int getChildCount(Object node) {
return isLeaf(node) ? 0 : ROOT_NUMBER;
}
#Override
public int getIndexOfChild(Object parent, Object child) {
int parentValue = ((Integer) parent).intValue();
int childValue = ((Integer) child).intValue();
return parentValue == ROOT_NUMBER ? childValue : -1 ;
}
public Object getRoot() {
return ROOT_NUMBER;
}
public boolean isLeaf(Object node) {
return ((Integer) node).intValue() < ROOT_NUMBER;
}
public void addTreeModelListener(TreeModelListener listener) { }
public void removeTreeModelListener(TreeModelListener listener) { }
public void valueForPathChanged(TreePath path, Object obj) { }
public static void display() {
JFrame frame = new JFrame("Number JTree");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
NumberTree tree = new NumberTree();
tree.setModel(new NumberTreeModel());
tree.setBackground(new Color(0,0,0,0));
JScrollPane scroll = new JScrollPane(tree);
frame.add(scroll);
scroll.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE);
tree.expandRow(0);
frame.pack();
frame.setSize(300, 400);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static class NumberTree extends JTree {
static final long serialVersionUID = 1;
#Override
public String convertValueToText(Object value, boolean selected, boolean expanded, boolean leaf,
int row, boolean hasFocus) {
if (value instanceof Integer) {
int n = ((Integer) value).intValue();
return n + "=========".substring(0, n % 10);
} else {
System.out.println("value class=" + value.getClass());
return value.toString();
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
display();
}
});
}
}
I suspect you have overridden paintComponent() but neglected super.paintComponent(g), as shown here.
Less likely, you might experiment with setScrollMode() in the scroll pane's viewport.
Editing your question to include an sscce might clarify the problem.
Addendum: For reference, here's an example that does not exhibit the rendering artifact seen in the question.
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
/**
* #see https://stackoverflow.com/a/15696825/230513
*/
public class Sscce {
private void display() {
JFrame f = new JFrame("Sscce");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTree tree = new JTree();
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}
f.add(new JScrollPane(tree));
f.pack();
f.setSize(200, 200);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new Sscce().display();
}
});
}
}
Solution:
Don't use setColor to change the background of the JTree with a non-opaque color.
For visual transparency of the nodes, implement a custom TreeCellRenderer to return null for getBackgroundNonSelectionColor and getBackground, as described in JTree set background of node to non-opaque
setting the viewport mode of the JScrollPane to JViewport.SIMPLE_SCROLL_MODE helps the scroll blur problem, but not necessarily repaint problems with expanding and collapsing ndoes.

Restore Expansion/Selection State in JTree with Lazy Loading

I have an application which contains a JTree backed with a DefaultTreeModel that is used to display a hierarchy of files which reflects a section of the file system on my server (I will refer to this as my client app). I also have a server application which provides the data that my client app needs to display (I will refer to this as my server app). I am using a "lazily load children" approach so that I only have to load files into my tree if the user is interested in them. Lazy load approach:
I override treeWillExpand(TreeExpansionEvent evt)
I set the selection path to be that of the expanding node.
Then I send a message to the server asking for the children of that node.
When the server responds I get the last selected path component.
Then I use DefaultTreeModel.insertNodeInto() for each the returned data files.
Finally I call DefaultTreeModel.nodeStructureChanged().
The above works fine and I am not having any issues with lazily loading the children. My problem comes when new data is uploaded to the server and I want to update the tree to not only include the new data, but also to set the expansion state and selected node to what it was prior to updating the tree (so that the user is not jerked around on the tree just because there is new data to view). The flow is as follows:
New data is uploaded to the server
Server app archives this data and populates a database with information about the uploaded files.
Server app notifies client app that new data was uploaded.
Client app saves the expansion state of the tree using JTree.getExpandedDescendants()
Client app saves the selection path of the tree using JTree.getSelectionPath()
Client app removes all nodes from the DefaultTreeModel.
Client app requests data from the server starting with the root node.
Client app traverses the tree path enumeration returned from JTree.getExpandedDescendants() calling JTree.expandPath() on each TreePath in the enumeration.
Client app sets the selected tree path.
My problem is that no matter what I try the tree's GUI is not updated to reflect the expansion state. I know that my call to expandPath is working because I can see the request for data sent from the client and the response with data from the server for each call to expandPath. I also display information about the currently selected node in another window and it is showing the correctly selected node. But, alas, to my disappointment, the GUI only displays the root node (expanded) and it's children (not expanded) instead of the previous expanded state.
So my question is: How can I restore the expansion state of my JTree so that the GUI remains the same before and after a data model update?
These are a few of the things that I have tried:
I found a thread with a similar setup to mine and his problem was solved by overriding equals() and hashCode() but that did not work for me.
Various methods to invoke expansion such as: setExpandsSelectedPaths(true), nodeStructureChanged(), JTree.invalidate()
Many different variations on saving the expansion state, however, I don't believe the expansion state is incorrect as I can see the correct data being passed back and forth between the client app and the server app.
Here is my SSCCE:
package tree.sscce;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.JButton;
import java.util.Enumeration;
import javax.swing.BoxLayout;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.JTextPane;
public class TreeSSCCE extends JFrame implements TreeWillExpandListener {
private static final long serialVersionUID = -1930472429779070045L;
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable() {
public void run() {
TreeSSCCE inst = new TreeSSCCE();
inst.setLocationRelativeTo(null);
inst.setVisible(true);
inst.setDefaultCloseOperation(EXIT_ON_CLOSE);
}
});
}
private DefaultMutableTreeNode rootNode;
private JTree tree;
private DefaultTreeModel treeModel;
private TreePath selectionPathPriorToNewData;
private Enumeration<TreePath> expandedPathsPriorToNewData;
private int treeSize = 5;
public TreeSSCCE() {
this.setBounds(0, 0, 500, 400);
JPanel mainPanel = new JPanel();
getContentPane().add(mainPanel, BorderLayout.CENTER);
mainPanel.setBounds(0, 0, 500, 400);
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
JPanel descriptionPanel = new JPanel();
descriptionPanel.setBounds(0, 0, 500, 200);
mainPanel.add(descriptionPanel);
JTextPane textPane = new JTextPane();
String newLine = System.getProperty("line.separator");
descriptionPanel.setLayout(new BorderLayout(0, 0));
textPane.setText("Start by expanding some nodes then click 'Add New Data' and you will notice that the tree state is not retained.");
descriptionPanel.add(textPane);
// Initialize The Tree
tree = new JTree();
rootNode = new DefaultMutableTreeNode("Root");
treeModel = new DefaultTreeModel(rootNode);
tree.addTreeWillExpandListener(this);
tree.setModel(treeModel);
tree.setShowsRootHandles(true);
populateTree(false);
JScrollPane scrollPane = new JScrollPane(tree);
mainPanel.add(scrollPane);
JPanel buttonPanel = new JPanel();
mainPanel.add(buttonPanel);
JButton btnAddNewData = new JButton("Add New Data");
btnAddNewData.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
addNewDataToTree();
}
});
buttonPanel.add(btnAddNewData);
}
private void removeAllTreeNodes()
{
while(!treeModel.isLeaf(treeModel.getRoot()))
{
treeModel.removeNodeFromParent((MutableTreeNode)treeModel.getChild(treeModel.getRoot(),0));
}
treeModel = null;
treeModel = new DefaultTreeModel(rootNode);
tree.setModel(treeModel);
}
public void restoreExpansionState(Enumeration enumeration)
{
if (enumeration != null)
{
while (enumeration.hasMoreElements())
{
TreePath treePath = (TreePath) enumeration.nextElement();
tree.expandPath(treePath);
tree.setSelectionPath(treePath);
}
tree.setSelectionPath(selectionPathPriorToNewData);
}
}
protected void addNewDataToTree()
{
// save the tree state
selectionPathPriorToNewData = tree.getSelectionPath();
expandedPathsPriorToNewData = tree.getExpandedDescendants(new TreePath(tree.getModel().getRoot()));
removeAllTreeNodes();
populateTree(true);
restoreExpansionState(expandedPathsPriorToNewData);
}
private void populateTree(boolean newData)
{
if(newData)
treeSize++;
MyParentNode[] parents = new MyParentNode[treeSize];
for(int i = 0; i < treeSize; i++)
{
parents[i] = new MyParentNode("Parent [" + i + "]");
treeModel.insertNodeInto(parents[i], rootNode, i);
}
}
#Override
public void treeWillCollapse(TreeExpansionEvent evt) throws ExpandVetoException {
// Not used.
}
#Override
public void treeWillExpand(TreeExpansionEvent evt) throws ExpandVetoException
{
System.out.println("Tree expanding: " + evt.getPath());
tree.setExpandsSelectedPaths(true);
tree.setSelectionPath(evt.getPath());
// we have already loaded the top-level items below root when we
// connected so lets just return...
if(evt.getPath().getLastPathComponent().equals(treeModel.getRoot()))
return;
// if this is not root lets figure out what we need to do.
DefaultMutableTreeNode expandingNode = (DefaultMutableTreeNode) evt.getPath().getLastPathComponent();
// if this node already has children then we don't want to reload so lets return;
if(expandingNode.getChildCount() > 0)
return;
// if this node has no children then lets add some
MyParentNode mpn = new MyParentNode("Parent Under " + expandingNode.toString());
treeModel.insertNodeInto(mpn, expandingNode, expandingNode.getChildCount());
for(int i = 0; i < 3; i++)
{
treeModel.insertNodeInto(new DefaultMutableTreeNode("Node [" + i + "]"), mpn, i);
}
}
private class MyParentNode extends DefaultMutableTreeNode
{
private static final long serialVersionUID = 433317389888990065L;
private String name = "";
public MyParentNode(String _name)
{
super(_name);
name = _name;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MyParentNode other = (MyParentNode) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
#Override
public boolean isLeaf()
{
return false;
}
private TreeSSCCE getOuterType() {
return TreeSSCCE.this;
}
}
}
Thanks in advance for any help you can provide.
P.S. This is my first question so please let me know if I am asking properly (and take it easy on me ;) ).
I am using a custom tree model (extends DefaultTreeModel) and reacting in the DBTreeEvent.STRUCTURE_CHANGED event to handle this. This is what I do to preserve the old state. Not sure if it will help you or not..
//NOTE: node is the tree node that caused the tree event
TreePath nodesPath = new TreePath(node.getPath());
TreePath currentSel = myTree.getLeadSelectionPath();
List<TreePath> currOpen = getCurrExpandedPaths(nodesPath);
super.nodeStructureChanged(node);
reExpandPaths(currOpen);
myTree.setSelectionPath(currentSel);
private List<TreePath> getCurrExpandedPaths(TreePath currPath)
{
List<TreePath> paths = new ArrayList<TreePath>();
Enumeration<TreePath> expandEnum = myTree.getExpandedDescendants(currPath);
if (expandEnum == null)
return null;
while (expandEnum.hasMoreElements())
paths.add(expandEnum.nextElement());
return paths;
}
private void reExpandPaths(List<TreePath> expPaths)
{
if(expPaths == null)
return;
for(TreePath tp : expPaths)
myTree.expandPath(tp);
}

Java TreePath.equals() trouble

I'm having trouble getting an Outline control (alternative TreeTable) to work with a tree of data objects, due to the method that TreePath uses to identify data nodes.
The key problem is that TreePath's equals() method uses the data nodes' equals() method to identify that two node objects are the same ones in the data tree. TreeModel.java even comments on this problem:
"Some implementations may assume that if two TreePaths are equal [as determined by equals()], they identify the same node. If this condition is not met, painting problems and other oddities may result." Example data:
A
B
C
D
B
E
F
H
K
Here, the two "B" nodes might, as individual nodes, be considered to have equal values (hence equals() returns true), but they certainly do not represent the same nodes in the tree.
OK, so if the source data objects have implemented equals() to indicate equal value considering just the node itself, this breaks TreePath if more than one node of the same value appears under a particular parent. In that case, Outline is unable to expand/collapse the correct one of the same-value nodes.
This problem would be solved if TreePath.equals() used "==" instead of data objects' equals() methods. However, since the stock TreePath is closely wired into TreeModel etc etc, it's not obvious how to go about repairing this behavior without a lot of disruption.
Is there some graceful way to get the right effect?
Thanks!
Actually, I think that the problem comes from the way you are implementing equals() in your TreeNode's. Two TreeNode's, in your case, should be considered equals if they represent the same visual nodes. Two TreeNode's can represent the same model object (for example Model Object B) but remain different nodes..
Here is a simple demo based on DefaultMutableTreeNode (equals() uses the Object.equals(Object) default implementation ==). Every 2 seconds it toggles selection from node B1 to B2:
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
public class TestTreeNodes {
public static class SomeModelNode {
private String value;
public SomeModelNode(String value) {
super();
this.value = value;
}
public String getValue() {
return value;
}
}
public class MyTreeCellRenderer extends DefaultTreeCellRenderer {
#Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
Component cell = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
if (value instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) value;
if (treeNode.getUserObject() instanceof SomeModelNode) {
setText(((SomeModelNode) treeNode.getUserObject()).getValue());
}
}
return cell;
}
}
private JFrame f;
private JTree tree;
private DefaultMutableTreeNode nodeA;
private DefaultMutableTreeNode nodeB1;
private DefaultMutableTreeNode nodeB2;
private DefaultMutableTreeNode nodeC;
private DefaultMutableTreeNode nodeD;
private DefaultMutableTreeNode nodeE;
private DefaultMutableTreeNode nodeF;
private DefaultMutableTreeNode nodeH;
private DefaultMutableTreeNode nodeK;
private boolean showingB1 = false;
protected void initUI() {
tree = new JTree(getModel());
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}
ToolTipManager.sharedInstance().registerComponent(tree);
tree.setCellRenderer(new MyTreeCellRenderer());
f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLocationRelativeTo(null);
f.add(new JScrollPane(tree));
f.pack();
f.setVisible(true);
Timer t = new Timer(2000, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (!showingB1) {
tree.getSelectionModel().setSelectionPath(getPathForNode(nodeB1));
} else {
tree.getSelectionModel().setSelectionPath(getPathForNode(nodeB2));
}
showingB1 = !showingB1;
}
});
t.start();
}
private TreePath getPathForNode(TreeNode node) {
List<TreeNode> nodes = new ArrayList<TreeNode>();
TreeNode current = node;
while (current != null) {
nodes.add(current);
current = current.getParent();
}
Collections.reverse(nodes);
return new TreePath(nodes.toArray(new Object[nodes.size()]));
}
private TreeModel getModel() {
SomeModelNode a = new SomeModelNode("A");
SomeModelNode b = new SomeModelNode("B");
SomeModelNode c = new SomeModelNode("C");
SomeModelNode d = new SomeModelNode("D");
SomeModelNode e = new SomeModelNode("E");
SomeModelNode f = new SomeModelNode("F");
SomeModelNode h = new SomeModelNode("H");
SomeModelNode k = new SomeModelNode("K");
nodeA = new DefaultMutableTreeNode(a);
nodeB1 = new DefaultMutableTreeNode(b);
nodeB2 = new DefaultMutableTreeNode(b);
nodeC = new DefaultMutableTreeNode(c);
nodeD = new DefaultMutableTreeNode(d);
nodeE = new DefaultMutableTreeNode(e);
nodeF = new DefaultMutableTreeNode(f);
nodeH = new DefaultMutableTreeNode(h);
nodeK = new DefaultMutableTreeNode(k);
// Children of A
nodeA.add(nodeB1);
nodeA.add(nodeB2);
nodeA.add(nodeH);
nodeA.add(nodeK);
// Children of B1
nodeB1.add(nodeC);
nodeB1.add(nodeD);
// Children of B2
nodeB2.add(nodeE);
nodeB2.add(nodeF);
DefaultTreeModel model = new DefaultTreeModel(nodeA);
return model;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TestTreeNodes().initUI();
}
});
}
}

How to get depth of current node in JTree?

I have a JTree with a few nodes and subnodes. When I click on a node I want to know on which depth is it (0, 1, 3). How can I know that?
selected_node.getDepth();
doesn't return the depth of current node..
You should be using getLevel. getLevel returns the number of levels above this node -- the distance from the root to this node. If this node is the root, returns 0. Alternatively, if for whatever reason you have obtained the Treenode[] path (using getPath()) then it is sufficient to take the length of that array.
getDepth is different, as it returns the depth of the tree rooted at this node. Which is not what you want.
basicaly you have to Iterate inside JTree, but TreeSelectionListener can returns interesting value, for example
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
public class TreeSelectionRow {
public TreeSelectionRow() {
JTree tree = new JTree();
TreeSelectionListener treeSelectionListener = new TreeSelectionListener() {
#Override
public void valueChanged(TreeSelectionEvent treeSelectionEvent) {
JTree treeSource = (JTree) treeSelectionEvent.getSource();
System.out.println("Min: " + treeSource.getMinSelectionRow());
System.out.println("Max: " + treeSource.getMaxSelectionRow());
System.out.println("Lead: " + treeSource.getLeadSelectionRow());
System.out.println("Row: " + treeSource.getSelectionRows()[0]);
}
};
tree.addTreeSelectionListener(treeSelectionListener);
String title = "JTree Sample";
JFrame frame = new JFrame(title);
frame.add(new JScrollPane(tree));
frame.setSize(300, 150);
frame.setVisible(true);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
TreeSelectionRow treeSelectionRow = new TreeSelectionRow();
}
});
}
}
If you have a TreeSelectionListener which handles the TreeSelectionEvent, you can use the TreeSelectionEvent#getPaths method to retrieve the selected TreePaths. The TreePath#getPathCount method returns the depth of the selected path.
You can also ask it directly to the JTree (although you will need the listener to be informed when the selection changes) by using the JTree#getSelectionPaths method.

Categories