Skip to main content

FileTree (JTree) and GUI delays

11 replies [Last post]
asailer
Offline
Joined: 2006-11-09

I have created a JTree with a custom TreeModel, FileTreeModel. It works as expected, and I've tried to make the code as efficient as possible.

When I expand a folder that contains many files, the GUI pauses - I know I shouldn't have code that takes awhile to execute do so on the Event Dispatching Thread, but how do I get around it?

I've just started to wrap my head around the new SwingWorker class version included in JDK 1.6, but I'm having difficulty in getting it to work for my purposes.

Expanding a directory causes the model to read the subfiles, and sort them alphabetically, directories first, then files.

Should I, and how could I move this method into a seperate thread? Or should I, and how do I, reliably change to the busy cursor for the few seconds of processing?

If I use a dedicated thread, I obviously need the repainting of the JTree to wait for the model to be updated, which is why I was lookiong at the new SwingWorker.

I'm searching forums and JavaRanch, but as the new SwingWorker is new to 1.6, I'm having difficulty figuring this out. Thanks in advance.

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
asailer
Offline
Joined: 2006-11-09

Thanks for the replies....

I have been reading some posts about the slowness of JTrees with ~100+ nodes. I am experiencing this as well. One post suggested both setting the large model attribute and making the row height fixed. What exactly is causing all the lag? Is it the painting of the cells upon node expansion? I've tried both with my custom cell renderer and the default tree cell renderer, so I don't think it's my code, per se. The individual methods in my treemodel are very fast, so I don't think the problem is there...

Any ideas?

asailer
Offline
Joined: 2006-11-09

Okay, it took some research and a couple of brain cells....

I put a System.out.println("blah") in my FileTreeModel's getChildren(Object parent) method.

It was then apparent that in the course of building/rendering the tree, that each cell rendered resulted in a call to getChild(...) and getChildCount(...), which each rely on getChildren(...).

Thus, I had to come up with a way to save state in order to prevent the duplication (actually, multiplication by the number of nodes) of work. The result is much much faster.

Expanding a directory of 1500 files went from about 3 minutes to ~ 1 second.

Thanks to everyone for ideas.... I'm posting my FileTreeModel for others to use/learn from.

This is also an example of a tree with checkboxes - although in my app I use popup menus to check/uncheck selected tree nodes instead of a custom editor.

[code]
________________________________________________________
// SepTreeModel.java
// Created on January 7, 2007, 2:29 PM
// @author Adam Sailer

package september;

import java.io.File;
import java.util.Arrays;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import shared.FileComparator;
import shared.Sifter;

public class SepTreeModel implements TreeModel
{
private File[] root;
private Object current;
private Object[] leaves;
private FileComparator comparator;
private Sifter sifter;

public SepTreeModel()
{
this.root = File.listRoots();
this.sifter = new Sifter();
this.comparator = new FileComparator();
}

public SepTreeModel(File[] root, Sifter sifter)
{
this.root = root;
this.sifter = sifter;
this.comparator = new FileComparator();
}

public Sifter sifter()
{
return this.sifter;
}

private Object[] getChildren(Object parent)
{
try
{
if (parent != current)
{
leaves = (Object[]) ((File) parent).listFiles(sifter);
Arrays.sort(leaves, comparator);
current = parent;
}
return leaves;
}
catch (Exception e)
{
// the root is this object (see getRoot())
return root;
}
}

@ Override
public void addTreeModelListener(TreeModelListener l)
{ }

@ Override
public Object getChild(Object parent, int index)
{
return getChildren(parent)[index];
}

@ Override
public int getChildCount(Object parent)
{
return getChildren(parent).length;
}

@ Override
public int getIndexOfChild(Object parent, Object child)
{
return Arrays.asList(getChildren(parent)).indexOf(child);
}

@ Override
public Object getRoot()
{
return this;
}

@ Override
public boolean isLeaf(Object node)
{
return (node != this && ((File) node).isFile());
}

@ Override
public void removeTreeModelListener(TreeModelListener l)
{ }

@ Override
public void valueForPathChanged(TreePath path, Object newValue)
{ }

}

_________________________________________________________
// Sort Directories First

// FileComparator.java
// Created on December 5, 2006, 10:46 AM
// @author Adam Sailer

package shared;

import java.io.File;
import java.util.Comparator;

public class FileComparator implements Comparator
{

public FileComparator()
{ }

@ Override
public int compare(Object ob0, Object ob1)
{
File zero = (File) ob0;
File one = (File) ob1;

if ((zero.isDirectory()) && (one.isFile()))
return -1;
else if ((zero.isFile()) && (one.isDirectory()))
return 1;
else
return zero.compareTo(one);
}
}

_________________________________________________________
// File filter

// Sifter.java
// Created on January 13, 2007, 10:10 PM
// @author Adam Sailer

package shared;

import java.io.File;
import java.io.FileFilter;

public class Sifter implements FileFilter
{
private String suffix = "@";

// Permit all files and directories
public Sifter()
{ }

// Permit directories and specified file extensions
public Sifter(String suffix)
{ this.suffix = suffix; }

@ Override
public boolean accept(File input)
{
try
{
return (permitted(input) && compare(input));
}
catch (Exception e)
{ return false; }
}

private boolean permitted(File input)
{
try
{
if (input.canRead())
{
// Determine whether symbolic link
String absolute = input.getAbsolutePath();
String canonical = input.getCanonicalPath();
return (absolute.equals(canonical));
}
return false;
}
catch (Exception e)
{ return false; }
}

private boolean compare(File input)
{
try
{
String[] parts = input.getName().split("\\.");
boolean match = parts[parts.length - 1].equalsIgnoreCase(suffix);
boolean any = suffix.equals("@");
return (input.isDirectory() || (match || any));
}
catch (Exception e)
{ return false; }
}

}

_________________________________________________________
// Renderer with bi-state CheckBoxes

// CellRenderer.java
// Created on January 22, 2007, 10:41 AM
// @author Adam Sailer

package september;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.io.File;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.tree.TreeCellRenderer;

public class SepCellRenderer implements TreeCellRenderer
{
private Icon plain = new ImageIcon(getClass().getResource("/icons/document.png"));

public SepCellRenderer()
{ }

public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus)
{
// Setup Label
JLabel label = new JLabel("");
label.setBorder(BorderFactory.createEmptyBorder(1,0,1,0));
label.setOpaque(false);

// Setup CheckBox
JCheckBox check = new JCheckBox("");
check.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
check.setOpaque(false);

// Setup Pane
JPanel pane = new JPanel();
pane.setLayout(new BorderLayout());
pane.setOpaque(false);
pane.add(label, BorderLayout.CENTER);

try
{
if (selected)
label.setForeground(new Color(255,255,255));

if (value instanceof File)
{
label.setText(((File) value).getName());

if (tree.getSelectionModel() instanceof SepSelectionModel)
{
boolean marked = ((SepSelectionModel) tree.getSelectionModel()).isSelected((File) value);
check.setSelected(marked);
pane.add(check, BorderLayout.WEST);
}
}
if (leaf)
label.setIcon(plain);
}
catch (Exception e)
{ }

return pane;
}

}

_________________________________________________________
// Selection Model that stores checkbox selections

// SepSelectionModel.java
// Created on January 13, 2007, 10:51 PM
// @author otters

package september;

import java.io.File;
import java.util.Collections;
import java.util.Vector;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreePath;
import shared.FileComparator;
import shared.Sifter;

public class SepSelectionModel extends DefaultTreeSelectionModel
{
private Vector stored;
private Sifter sifter;

public SepSelectionModel()
{
super();
stored = new Vector();
sifter = new Sifter();
}

public SepSelectionModel(Sifter sifter)
{
super();
stored = new Vector();
this.sifter = sifter;
}

public boolean isSelected(File input)
{
return stored.contains(input);
}

public Vector stored()
{
return stored;
}

public void storeSelected()
{
try
{
TreePath[] paths = this.getSelectionPaths();
for (TreePath path : paths)
storeDescendants(path);

Collections.sort(stored, new FileComparator());
}
catch (Exception e)
{ }
}

public void clearSelected()
{
try
{
TreePath[] paths = this.getSelectionPaths();
for (TreePath path : paths)
clearDescendants(path);
}
catch (Exception e)
{ }
}

private void storeDescendants(TreePath path)
{
try
{
Vector current = descendants((File) path.getLastPathComponent());
for (Object ob : current)
if (!stored.contains((File) ob))
stored.add((File) ob);
}
catch (Exception e)
{ }
}

private void clearDescendants(TreePath path)
{
try
{
Vector current = descendants((File) path.getLastPathComponent());
for (Object ob : current)
stored.remove((File) ob);
}
catch (Exception e)
{ }
}

private Vector descendants(File input)
{
Vector out = new Vector();
try
{
out.add(input);
File[] array = input.listFiles(sifter);
for (File file : array)
out.addAll(descendants(file));
}
catch (Exception e)
{ }
return out;
}

// Test Code
private void print()
{
System.out.println();
for (Object ob : stored)
System.out.println(((File) ob).getName());
System.out.println();
}
}
[/code]

Message was edited by: asailer

Message was edited by: asailer

kirillcool
Offline
Joined: 2004-11-17

Can you use the code tags (use square brackets) so that the code is more readable?

Like this (without spaces):

[ code ]
your code here
[ / code ]

asailer
Offline
Joined: 2006-11-09

Sweet!

Thanks for the tip - much better!

- Adam

Scott Violet

Just imagine if you expand a network path that is slow and takes 5
minutes to load. You definitely do NOT want the GUI dead for that long.
The way to address this with TreeModel is to override the getChildCount
method. When getChildCount is invoked, if you haven't loaded the
directory yet, spawn a thread and load it in the background. The only
trick is to make sure you send out notification of nodes being added on
the EDT. If you don't, you're violating Swing's threading rules, and
will get hurt;)

Here's some code I wrote for one of the guys that used to be on the team
a couple of years back. He wanted to pause after each file, so the code
has unnecessary sleeps in it. That can be removed, and I would also add
all the children at once, instead of file by file.

Here's the code. Perhaps I'll turn this into a blog one of these days.

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.util.*;

public class ThreadingTest {
public static void main(String[] args) {
TreeModel model = new FileTreeModel();
JTree tree = new JTree(model);
JFrame frame = new JFrame();
frame.add(new JScrollPane(tree));
frame.setBounds(0, 0, 400, 400);
frame.show();
}

// A simple TreeModel that delegates to instances of CachedFile
private static class FileTreeModel implements TreeModel {
private EventListenerList listenerList = new EventListenerList();
private CachedFile root;

FileTreeModel() {
root = new CachedFile(new File("/"), null);
}

public Object getRoot() {
return root;
}

public Object getChild(Object parent, int index) {
return ((CachedFile)parent).getChild(index);
}

public int getChildCount(Object parent) {
return ((CachedFile)parent).getChildCount();
}

public boolean isLeaf(Object node) {
return ((CachedFile)node).isLeaf();
}

public void valueForPathChanged(TreePath path, Object newValue) {
}

public int getIndexOfChild(Object parent, Object child) {
return ((CachedFile)parent).getIndexOfChild(child);
}

public void addTreeModelListener(TreeModelListener l) {
listenerList.add(TreeModelListener.class, l);
}

public void removeTreeModelListener(TreeModelListener l) {
listenerList.remove(TreeModelListener.class, l);
}

// A TreeNode like class that wraps a File. The children are
// loaded in a background thread when getChildCount is invoked.
private class CachedFile {
// Whether or not we've loaded the children
private boolean loaded;
// Whether or not we're in the process of loading
private boolean loading;
// The children
private ArrayList children;
// File we reference
private File file;
// Parent
private CachedFile parent;

CachedFile(File file, CachedFile parent) {
this.file = file;
this.parent = parent;
loaded = false;
children = new ArrayList();
}

//
// Mark, these are the 3 key methods, getChildCount,
// load and add.
//
public int getChildCount() {
// Mark, this is the key, only start loading if you
// haven't loaded and are not in the process of loading.
boolean shouldLoad = false;
synchronized(this) {
if (!loaded && !loading) {
loading = true;
shouldLoad = true;
}
}
if (shouldLoad) {
load();
}
return children.size();
}

private void load() {
System.out.println("loading: " + file);
// Loading spawns a thread to do the loading, nodes are
// added on the EDT and notification is then sent out.
new Thread(new Runnable() {
public void run() {
File[] files = file.listFiles();
for (int counter = 0; counter < files.length;
counter++) {
final File file = files[counter];
// Do a sleep here to show the effect you are
// after
try {
Thread.sleep(70);
} catch (InterruptedException ie) {
}
// Do actual insertion on EDT.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
add(file);
}
});
}
System.out.println("finished loading: " + file);
synchronized(CachedFile.this) {
// Update state to indicate everything is
loaded
loaded = true;
loading = false;
}
}
}).start();
}

private void add(File file) {
// Add to internal list
children.add(new CachedFile(file, this));

// send notification
Object[] listeners = listenerList.getListenerList();
TreeModelEvent e = new TreeModelEvent(
FileTreeModel.this, getPath(),
new int[] { children.size() - 1},
new Object[] { children });
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==TreeModelListener.class) {
((TreeModelListener)listeners[i+1]).
treeNodesInserted(e);
}
}
}

public Object getChild(int index) {
return children.get(index);
}

public boolean isLeaf() {
return file.isFile();
}

public int getIndexOfChild(Object child) {
return children.indexOf(child);
}

private Object[] getPath() {
return getPathToRoot(this, 0);
}

private Object[] getPathToRoot(CachedFile aNode, int depth) {
Object[] retNodes;
if(aNode == null) {
if(depth == 0)
return null;
else
retNodes = new Object[depth];
}
else {
depth++;
if(aNode == root)
retNodes = new Object[depth];
else
retNodes = getPathToRoot(aNode.parent, depth);
retNodes[retNodes.length - depth] = aNode;
}
return retNodes;
}

public String toString() {
return file.toString();
}
}
}
}

-Scott

leouser
Offline
Joined: 2005-12-12

Id guess the rule of thumb for deciding when the GUI should be eaten and not eaten is if the task you are executing is essential to proceed to the next step of the workflow. Probably in either case, it will be necessary to provide feedback to the user how the task is proceeding.

leouser

asailer
Offline
Joined: 2006-11-09

That was way over my head, but thanks....

leouser
Offline
Joined: 2005-12-12

uh oh,

TreeExpansionListener allows you to listen in when the tree is going to expand/collapse. It also allows you to veto the process.

GlassPane, is on the JRootPane. If you set it to visible and enable mouse events, you should be able to block mouse input for the duration of the operation. You can probably set the cursor on this and have it be the one you want as well.

The reading work, that's up to you. At some point you'd have to make the glass pane invisible again and allow the tree to expand.

I hope that's a little clearer(I suppose code would be the best),
leouser

asailer
Offline
Joined: 2006-11-09

Found a solution through trial and error (mostly error):

I was willing to settle for a "busy" mouse cursor since execution was only taking 3 or 4 seconds max (possibly longer on a slower machine). This isn't bad, since the delays only happen on the first expansion of a directory that contains many files - subsequent expands of the same directory are instantaneous. So, here's how I managed the "busy" icon for those short (and infrequent) delays:

...
// This section done in NetBeans Form Editor

tree.addTreeWillExpandListener(new javax.swing.event.TreeWillExpandListener()
{
public void treeWillCollapse(javax.swing.event.TreeExpansionEvent evt)throws javax.swing.tree.ExpandVetoException
{
}
public void treeWillExpand(javax.swing.event.TreeExpansionEvent evt)throws javax.swing.tree.ExpandVetoException
{
treeTreeWillExpand(evt);
}
});
tree.addTreeExpansionListener(new javax.swing.event.TreeExpansionListener()
{
public void treeCollapsed(javax.swing.event.TreeExpansionEvent evt)
{
treeTreeCollapsed(evt);
}
public void treeExpanded(javax.swing.event.TreeExpansionEvent evt)
{
treeTreeExpanded(evt);
}
});

...

private void treeTreeWillExpand(javax.swing.event.TreeExpansionEvent evt)throws javax.swing.tree.ExpandVetoException
{
tree.setCursor(new Cursor(Cursor.WAIT_CURSOR));
}

private void treeTreeExpanded(javax.swing.event.TreeExpansionEvent evt)
{
tree.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}

...

asailer
Offline
Joined: 2006-11-09

// FileTreeModel.java
// Created on December 5, 2006, 9:09 AM

import java.io.File;
import java.util.Arrays;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public class FileTreeModel implements TreeModel
{
private File[] root;

public FileTreeModel()
{ }

public FileTreeModel(File[] root)
{
this.root = root;
}

@Override
public void addTreeModelListener(TreeModelListener input)
{ }

@Override
public Object getChild(Object parent, int index)
{
Object[] children = getChildren(parent);
return children[index];
}

private Object[] getChildren(final Object parent)
{
// the root is this object (see getRoot())
if (parent == this)
return root;
else
{
Object[] array = (Object[]) ((File) parent).listFiles();
Arrays.sort(array, new FileComparator());
return array;
}
}

@Override
public int getChildCount(Object parent)
{
return getChildren(parent).length;
}

@Override
public int getIndexOfChild(Object parent, Object child)
{
Object[] children = getChildren(parent);
return Arrays.asList(children).indexOf(child);
}

@Override
public Object getRoot()
{
return this;
}

@Override
public boolean isLeaf(Object node)
{
return (node != this && ((File) node).isFile());
}

@Override
public void removeTreeModelListener(TreeModelListener input)
{ }

@Override
public void valueForPathChanged(TreePath path, Object newValue)
{ }

}

leouser
Offline
Joined: 2005-12-12

you can probably veto the process with the TreeExpansionListener class, spin off a loading task, suspend mouse input with your friend the glasspane and finally expand the loaded node after everything is done. If you use SwingWorker that's up to you. There's more than one way to thread in Java.

leouser