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();
}
});
}
}
Related
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.
In my project i have a Jtree with custom node (which extends DefaultMutableTreeNode). Each node is associated with a boolean value. When the boolean is False i woul like to strike the texte of my node. So for example :
node 1
node1.1
node1.2
node 2
node2.1
...
I tried to create a new Font but i don't find any properties to strike the text and i only managed to add my custom font to the whole tree and not node by node.
I think that i should use the TreeRenderer but i can't find any method to help me strike the node.
Does someone have an idea on i can do it ?
Check out the example below. For keeping it simple, I am just striking through the selected node. You need to, of course, use your own check on the value.
package snippet;
import java.awt.Component;
import java.awt.Font;
import java.awt.font.TextAttribute;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultTreeCellRenderer;
public class JTreeTest extends JFrame {
private final class StrikeoutCellRenderer extends DefaultTreeCellRenderer {
private static final long serialVersionUID = 1L;
#Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
Component c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
Font font = c.getFont();
Map attributes = font.getAttributes();
if(sel)
attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
else
attributes.remove(TextAttribute.STRIKETHROUGH);
Font newFont = new Font(attributes);
c.setFont(newFont);
return c;
}
}
private static final long serialVersionUID = 1L;
public JTreeTest() {
super(JTreeTest.class.getName());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
initComponents();
}
private void initComponents() {
JTree tree = new JTree();
tree.setCellRenderer(new StrikeoutCellRenderer());
add(tree);
pack();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override public void run() {
JTreeTest t = new JTreeTest();
t.setVisible(true);
}
});
}
}
Note that even if the node doesn't need a strike through, you need to reset the attribute, since a single component is used for painting all the nodes.
The simplest way would be to define a renderer (extending DefaultTreeCellRenderer) and call setText() passing html string like this "<html><u>node1.2</u></html>" as value for the strikethrough.
I am using a custom TreeModel for my JTree. I have a root node and only 1 child node that is retrieved by a query from the database. I am able to populate the tree with the desired output.
However, when I click on the child node, it keeps recursively dispalying the same child node and it keeps adding child nodes with the same output. I tried to use static nodes i.e. I created a root node and then added 2 child nodes to it and I observe the same behavior.
My main program
import javax.swing.JFrame;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
public class RunApp {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
ShowFrame f = new ShowFrame();
f.setSize(600, 600);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}
});
}
}
My show_frame class
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.HeadlessException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
public class ShowFrame extends JFrame {
private JSplitPane splitPane;
private FormPanel formPanel;
private TreePanel treePanel;
private JTabbedPane tabPane;
private List<Objects> instanceDetails= new ArrayList<Objects>();
public ShowFrame() {
super("new frame");
formPanel = new FormPanel();
instanceDetails.add(new Objects(" "," "," "," "));
treePanel = new TreePanel(instanceDetails);
tabPane = new JTabbedPane();
tabPane.add(treePanel);
splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, formPanel,
tabPane);
splitPane.setOneTouchExpandable(true);
setMinimumSize(new Dimension(500, 500));
add(splitPane, BorderLayout.CENTER);
}
}
This is where I create my TreePanel
import java.util.List;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
public class TreePanel extends JPanel {
private int count = 0;
private JTree tree;
private List<Objects> instanceDetails;
private MyTreeModel gm;
private DefaultMutableTreeNode root = new DefaultMutableTreeNode();
private Controller c = new Controller();
public TreePanel(List<Objects> instanceDetails) {
this.instanceDetails = instanceDetails;
tree = new JTree();
if (instanceDetails.get(0).getObjectId() == " ") {
tree.setModel(new MyTreeModel(root));
} else {
tree.setModel(new MyTreeModel(treeNodes(instanceDetails)));
}
gm = new MyTreeModel(root);
gm.fireTreeStructureChanged(root);
tree.getSelectionModel().setSelectionMode(
TreeSelectionModel.SINGLE_TREE_SELECTION);
add(tree);
}
private DefaultMutableTreeNode treeNodes(List<Objects> instanceDetails) {
for (Objects id : instanceDetails) {
count++;
DefaultMutableTreeNode objs = new DefaultMutableTreeNode(count + " : " + id.getType()
+ " : " + id.getObjectId() + " : " + id.getStatus() + " : "
+ id.getCondition());
root.add(objs);
}
return root;
}
}
My tree model
import java.util.Vector;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
public class MyTreeModel implements TreeModel {
public static Vector<TreeModelListener> treeModelListeners =
new Vector<TreeModelListener>();
private static DefaultMutableTreeNode rootPerson;
public MyTreeModel(DefaultMutableTreeNode nodes) {
rootPerson = nodes;
}
//////////////// Fire events //////////////////////////////////////////////
/**
* The only event raised by this model is TreeStructureChanged with the
* root as path, i.e. the whole tree has changed.
*/
protected void fireTreeStructureChanged(DefaultMutableTreeNode rootPerson) {
TreeModelEvent e = new TreeModelEvent(this, new Object[] {rootPerson});
for (TreeModelListener tml : treeModelListeners) {
tml.treeStructureChanged(e);
}
}
//////////////// TreeModel interface implementation ///////////////////////
/**
* Adds a listener for the TreeModelEvent posted after the tree changes.
*/
public void addTreeModelListener(TreeModelListener l) {
treeModelListeners.addElement(l);
}
/**
* Returns the child of parent at index index in the parent's child array.
*/
public Object getChild(Object parent, int index) {
return rootPerson.getChildAt(index);
}
/**
* Returns the number of children of parent.
*/
public int getChildCount(Object parent) {
return 1;
//rootPerson.getLeafCount()
}
/**
* Returns the index of child in parent.
*/
public int getIndexOfChild(Object parent, Object child) {
return rootPerson.getIndex((DefaultMutableTreeNode) child);
}
/**
* Returns the root of the tree.
*/
public Object getRoot() {
return rootPerson;
}
/**
* Returns true if node is a leaf.
*/
public boolean isLeaf(Object node) {
return rootPerson.isLeaf();
}
/**
* Removes a listener previously added with addTreeModelListener().
*/
public void removeTreeModelListener(TreeModelListener l) {
//removeTreeModelListener(l);
}
/**
* Messaged when the user has altered the value for the item
* identified by path to newValue. Not used by this model.
*/
public void valueForPathChanged(TreePath path, Object newValue) {
}
}
Your implementation of TreeModel is clumsy and is the cause of your issues:
public static Vector<TreeModelListener> treeModelListeners =
new Vector<TreeModelListener>();
private static DefaultMutableTreeNode rootPerson;
--> Bad, Bad, Bad, ... real bad. There is absolutely no need to make these statements static and this will cause severe issues if you happen to create 2 different instances
/**
* Returns the child of parent at index index in the parent's child array.
*/
public Object getChild(Object parent, int index) {
return rootPerson.getChildAt(index);
}
Here, no matter which parent is provided, you return always the same child (hence this is why you see the same child over and over). The code should be return (parent==rootPerson?rootPerson.getChildAt(index):null);
/**
* Returns the number of children of parent.
*/
public int getChildCount(Object parent) {
return 1;
//rootPerson.getLeafCount()
}
Same as previous comment, you don't look what is the parent. Code should be return (parent==rootPerson?1:0);
/**
* Returns the index of child in parent.
*/
public int getIndexOfChild(Object parent, Object child) {
return rootPerson.getIndex((DefaultMutableTreeNode) child);
}
Same as previous comment, you don't look what is the parent. Code should be return (parent==rootPerson?rootPerson.getIndex((DefaultMutableTreeNode) child):-1);
/**
* Returns true if node is a leaf.
*/
public boolean isLeaf(Object node) {
return rootPerson.isLeaf();
}
Again, same mistake, you don't care about node
/**
* Removes a listener previously added with addTreeModelListener().
*/
public void removeTreeModelListener(TreeModelListener l) {
//removeTreeModelListener(l);
}
Why don't you implement properly removeTreeModelListener? (and as suggested by #trashgod, you can always use the default EventListenerList which does most of the work for you)
Conclusion: your implementation of TreeModel is full of bugs and this is why you get the problem you describe. Now, since you are using DefaultMutableTreeNode, I can only encourage you to also use DefaultTreeModel which will handle everything for you and avoid you to have to re-implement this, with all the "risks" it implies.
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);
}
Please have a look at the SSCCE. How can I make the non-selected tree nodes' background transparent. At the moment the background of non-selected nodes is white. My cell renderer, however, should paint it non-opaque if it is not selected (and green when selected...what it does). In the end I want non-selected nodes to be just text without background, since the area which is red in the SSCCE has a gradient fill in my application.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
public class SimpleTree extends JFrame
{
public static void main(final String[] args)
{
new SimpleTree();
}
public SimpleTree()
{
super("Creating a Simple JTree");
final Container content = this.getContentPane();
content.setBackground(Color.RED);
final Object[] hierarchy = { "javax.swing", "javax.swing.border", "javax.swing.colorchooser", "javax.swing.event", "javax.swing.filechooser", new Object[] { "javax.swing.plaf", "javax.swing.plaf.basic", "javax.swing.plaf.metal", "javax.swing.plaf.multi" }, "javax.swing.table",
new Object[] { "javax.swing.text", new Object[] { "javax.swing.text.html", "javax.swing.text.html.parser" }, "javax.swing.text.rtf" }, "javax.swing.tree", "javax.swing.undo" };
final DefaultMutableTreeNode root = this.processHierarchy(hierarchy);
final JTree tree = new JTree(root);
tree.setOpaque(false);
tree.setCellRenderer(new MyCellRenderer());
final JScrollPane scroller = new JScrollPane(tree);
scroller.getViewport().setOpaque(false);
scroller.setOpaque(false);
content.add(scroller, BorderLayout.CENTER);
this.setSize(275, 300);
this.setVisible(true);
}
/**
* Small routine that will make node out of the first entry in the array,
* then make nodes out of subsequent entries and make them child nodes of
* the first one. The process is repeated recursively for entries that are
* arrays.
*/
private DefaultMutableTreeNode processHierarchy(final Object[] hierarchy)
{
final DefaultMutableTreeNode node = new DefaultMutableTreeNode(hierarchy[0]);
DefaultMutableTreeNode child;
for (int i = 1; i < hierarchy.length; i++)
{
final Object nodeSpecifier = hierarchy[i];
if (nodeSpecifier instanceof Object[]) // Ie node with children
child = this.processHierarchy((Object[]) nodeSpecifier);
else
child = new DefaultMutableTreeNode(nodeSpecifier); // Ie Leaf
node.add(child);
}
return (node);
}
public class MyCellRenderer extends DefaultTreeCellRenderer
{
#Override
public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus)
{
final Component ret = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
final DefaultMutableTreeNode node = ((DefaultMutableTreeNode) (value));
this.setText(value.toString());
if (sel)
{
this.setOpaque(true);
this.setBackground(Color.GREEN);
}
else
{
this.setOpaque(false);
this.setBackground(null);
}
return ret;
}
}
}
You should override getBackgroundNonSelectionColor,getBackgroundSelectionColor and getBackground of DefaultTreeCellRenderer and return appropriate values like so:
public class MyCellRenderer extends DefaultTreeCellRenderer {
#Override
public Color getBackgroundNonSelectionColor() {
return (null);
}
#Override
public Color getBackgroundSelectionColor() {
return Color.GREEN;
}
#Override
public Color getBackground() {
return (null);
}
#Override
public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) {
final Component ret = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
final DefaultMutableTreeNode node = ((DefaultMutableTreeNode) (value));
this.setText(value.toString());
return ret;
}
}
which will produce:
Other suggestions:
Create and manipulate Swing components on Event Dispatch Thread.
Dont extend JFrame unnecessarily rather create an instance and use that.
Dont call setSize on JFrame rather use a correct LayoutManager and/or override getPreferredSize() and call pack() on JFrame before setting it visible but after adding all components.
Remember to call JFrame#setDefaultCloseOperation with either DISPOSE_ON_CLOSE or EXIT_ON_CLOSE (DISPOSE_XXX is usually preferred unless using Timers as this will allow main(String[] args) to continue its execution after Gui has been closed).
To avoid background refilling, just put UIManager.put("Tree.rendererFillBackground", false); before new SimpleTree(); or after super("Creating a Simple JTree");.