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.
Related
I need to close all the children and parent windows when I close one window in the hierarchy.
I have three lines of windows:
My windows hierarchy - All lines start in one MainWindow. Only LoadPlayers and CreatePlayers are dialogs. All other windows are frames.
E.g. I close TablesOverview in the first line - I need to close all other windows in this line.
But windows in other lines must stay open.
Notice that TablesOverview is in two lines.
I can write the code, where I named every window that must close. But I need cleaner solution.
This code give me all opened windows - I don't know how to take only windows in one line.
Window[] windows = Window.getWindows();
This codes give me nothing.
Window[] windows = frame.getOwnedWindows();
Component comp = frame.getParent();
Java don't provides this support implicitly. So, you have to use some work around:
Solution:
Create a new class which extends JFrame
Create a data member children of type ArrayList<Your class name>. This holds the instance created by current window
Create an member method createChildWindow(). This method will create a child window and add it in to variable children
Register for event on closing the window. For this use addWindowListener() and override public void windowClosing(java.awt.event.WindowEvent windowEvent) method. In this method traverse throw the children collection and close each child(This will eventually close its subsequent children also).
Following is the complete working code
WindowTracker.java
package com.cse.test.awt;
import java.awt.FlowLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class WindowTracker extends JFrame
{
ArrayList<WindowTracker> children;
JLabel childrenCount;
public WindowTracker()
{
children = new ArrayList<>();
this.addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent arg0) {
System.out.println("Closing..." + this.hashCode());
for(WindowTracker child:children)
{
child.dispatchEvent(new WindowEvent(child, WindowEvent.WINDOW_CLOSING));
}
System.out.println("Closed..." + this.hashCode());
}
});
this.setLayout(new FlowLayout(FlowLayout.RIGHT));
childrenCount = new JLabel("Children count: 0");
this.add(childrenCount);
JButton btn = new JButton("Create new child");
btn.addActionListener(e -> {WindowTracker child = getNewChild();});
this.add(btn);
this.setSize(300, 300);
this.setTitle("" + this.hashCode());
this.setVisible(true);
}
public WindowTracker getNewChild()
{
WindowTracker child = new WindowTracker();
children.add(child);
childrenCount.setText("Children count: " + children.size());
return child;
}
}
Executer.java
package com.cse.test.common;
import com.cse.test.awt.WindowTracker;
public class Executer {
public static void main(String[] args) {
// TODO Auto-generated method stub
new WindowTracker();
}
}
Exploring the JTree component, I wrote a small class that will list the directories and files of my hard disk. To avoid a "full scan" which would takes longs minutes and would be a waste of time and ressources, I decided that I'll explore only 1 sub level of the "active node". By "active node", I mean the directory clicked OR the node expanded.
For the directory clicked, it works perfectly : I can explore my directories and subdirectories, the code works "sub level by sub level" and the directories clicked appears like directories.
But if I expand a node, it fail ! The method to explore the children of this nodes runs ; it find all the children and can list them via a "System.out.println(...)", but my directories still appears like files even if they have children. To have the directory appears like a directory, I have to click on it (=using the "first method").
Here is my code. Does someone could explain me what is failing ?
import java.awt.BorderLayout;
import java.io.File;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
public class Explorer2 extends JPanel {
private JTree tree;
private DefaultMutableTreeNode root= new DefaultMutableTreeNode();
int countSubLevel=0;
int limit=1;
Explorer2() {
DefaultMutableTreeNode driveNode=null;
for (File file:File.listRoots()) {
driveNode = new DefaultMutableTreeNode(file.getAbsolutePath());
//testLeaf(driveNode);
exploreDirectory(file, driveNode);
root.add(driveNode);
}
displayTree();
}
Explorer2(String rootDirectory) {
File file = new File(rootDirectory);
DefaultMutableTreeNode directoryNode = new DefaultMutableTreeNode(file.getAbsolutePath());
exploreDirectory(file, directoryNode);
root.add(directoryNode);
displayTree();
}
public void exploreDirectory (File dir, DefaultMutableTreeNode dirNode) {
if (dir.isDirectory()) {
//System.out.println(dir+" (parent) is a directory. Its level is : "+dirNode.getLevel());
try {
for (File file:dir.listFiles()) {
DefaultMutableTreeNode fileNode = new DefaultMutableTreeNode(file.getAbsolutePath());
dirNode.add(fileNode);
//System.out.println(dirNode+" - "+file+" - Level = "+fileNode.getLevel());
/*
if (fileNode.getLevel()<limit) {
System.out.println(file+" (child) have a level : "+fileNode.getLevel());
exploreDirectory(file, fileNode);
} else {
//System.out.println("Not in the loop : "+fileNode.getLevel());
}
*/
}
} catch (NullPointerException e) {
System.err.println(dir+" generates a NullPointerException");
}
} else {
System.out.println(dir+" is a file. Its level is : "+dirNode.getLevel());
}
countSubLevel+=1;
}
public void displayTree () {
DefaultTreeModel treeModel = new DefaultTreeModel(root);
tree = new JTree(treeModel);
//tree = new JTree(root);
tree.setRootVisible(true);
tree.addTreeSelectionListener(new MyTreeSelectionListener());
tree.addTreeExpansionListener(new MyTreeExpansionListener());
this.setLayout(new BorderLayout());
this.add(new JScrollPane(tree), BorderLayout.CENTER);
}
public void testLeaf(DefaultMutableTreeNode dir) {
if (dir.isLeaf()) {
System.out.println("Rien en dessous de "+dir);
} else {
System.out.println("Creuse !");
}
}
class MyTreeSelectionListener implements TreeSelectionListener {
#Override
public void valueChanged(TreeSelectionEvent arg0) {
if (tree.getLastSelectedPathComponent() != null) {
File dir = new File(tree.getLastSelectedPathComponent().toString());
DefaultMutableTreeNode dirNode = (DefaultMutableTreeNode) arg0.getPath().getLastPathComponent();
DefaultMutableTreeNode fileNode=null;
//System.out.println(dirNode.getChildCount());
if (dirNode.getChildCount()==0) {
System.out.println("The directory is : "+dir+" - Node : "+dirNode);
exploreDirectory(dir, dirNode);
}
}
}
}
class MyTreeExpansionListener implements TreeExpansionListener {
#Override
public void treeCollapsed(TreeExpansionEvent arg0) {
System.out.println("Collapsed : "+arg0.getPath().getLastPathComponent());
}
#Override
public void treeExpanded(TreeExpansionEvent arg0) {
DefaultMutableTreeNode dir = (DefaultMutableTreeNode) arg0.getPath().getLastPathComponent();
DefaultMutableTreeNode fileNode=null;
System.out.println("Expanded directory is : "+dir+" - Number of child : "+dir.getChildCount());
for (int i=0 ; i<dir.getChildCount() ; i++) {
File file = new File(dir.getChildAt(i).toString());
fileNode = new DefaultMutableTreeNode(file.getAbsolutePath());
exploreDirectory(file, fileNode);
System.out.println("*"+i+" - directory is : "+dir+" - Files are : "+file+" - Number of children : "+fileNode.getChildCount());
}
}
}
public static void main(String[] args) {
JFrame window = new JFrame ();
window.setSize(500, 600);
window.setTitle("Explorateur");
window.setLocationRelativeTo(null);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setLayout(new BorderLayout());
String str = System.getProperty("user.home");
//Explorer2 explorer = new Explorer2(str);
Explorer2 explorer = new Explorer2();
window.getContentPane().add(explorer);
window.setVisible(true);
}
}
Thanks to all the answers. Regards.
You need to use a TreeModel class, such as DefaultTreeModel, to serve as the model for the nodes in the tree. It has methods for the system to determine if a given node is a leaf or not, and whether to allow any node to contain children or whether (as in your case) only certain nodes can contain children.
Your program doesn't use a model of its own, so the JTree just creates its own; since there's no way for the model it creates to tell whether a node is a leaf or not unless it's expanded, then it doesn't know it's a parent node with children until it's expanded and the listener adds nodes to it.
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.
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);
}