It seems JTree has a -for me- unexpected behavior: it relies on the hashcode of the nodes instead of relying on its model. This deviates from the way e.g. JTable and JList work: these two allow you to provide some value via their models and then have a renderer to visualize it. JTree does not, and I would like to understand why (and if possible how to circumvent it).
Below an example, based on a JavaBean "person".
public class Person {
String name;
public String getName() {
return this.name;
}
public void setName(String v) {
this.name = v;
}
@Override
public int hashCode() {
// return super.hashCode();
return java.util.Objects.hash(name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Person other = (Person)obj;
return java.util.Objects.equals(this.name, other.name);
}
@Override
public String toString() {
return super.toString() + "#" + System.identityHashCode(this)
+ ",name=" + name;
}
}
And
public class JTreeTest {
static public void main(String[] args) throws Exception {
SwingUtilities.invokeAndWait(() -> {
Person personCarl = new Person();
personCarl.setName("carl");
Person personMarry = new Person();
personMarry.setName("marry");
List<Person> persons = new ArrayList<>();
persons.add(personCarl);
persons.add(personMarry);
JTree jTree = new JTree(new TreeModel(){
@Override
public Object getRoot() {
return persons;
}
@Override
public Object getChild(Object parent, int index) {
if (parent == persons) {
return persons.get(index);
}
return null;
}
@Override
public int getChildCount(Object parent) {
if (parent == persons) {
return persons.size();
}
return 0;
}
@Override
public boolean isLeaf(Object node) {
if (node == persons) {
return false;
}
return true;
}
@Override
public void valueForPathChanged(TreePath path, Object newValue) {
}
@Override
public int getIndexOfChild(Object parent, Object child) {
if (parent == persons) {
return persons.indexOf(child);
}
return 0;
}
@Override
public void addTreeModelListener(TreeModelListener l) {
}
@Override
public void removeTreeModelListener(TreeModelListener l) {
}
});
jTree.setCellRenderer(new DefaultTreeCellRenderer(){
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
Component component = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
JLabel jLabel = (JLabel)component;
if (value == persons) {
jLabel.setText("ROOT");
}
else {
Person person = (Person)value;
jLabel.setText(person.getName());
}
return component;
}
});
JButton button = new JButton("change it");
button.addActionListener(e -> {
personCarl.setName(personCarl.getName() + "x");
System.out.println(personCarl);
});
JFrame jFrame = new JFrame();
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.getContentPane().setLayout(new FlowLayout());
jFrame.getContentPane().add(new JScrollPane(jTree));
jFrame.getContentPane().add(button);
jFrame.pack();
jFrame.setVisible(true);
});
}
}
The application implements a simple TreeModel, sets up the renderer (you can leave that out if you want - it does not change anything), and once started it displays the tree. If you click the button, the name changes, so the bean's hashcode, and the tree stops painting.
The solution is to have some kind of intermediate "Node" class. But this should not be necessary, the model provides all the information the tree needs. This pattern works perfectly fine on JTable or JList.
Replacing the hashcode with an unchanging one (just swap the lines in the method) makes it all work.
The TreeModel implementation in the question is broken in failing to notify its listeners about changes. After adding the notification - admittedly very coarse in adding api to be used by application code (!don't! - it's model responsibility) that fires a structureChanged for the whole tree - none of the problems is visible:
and using in the change action:
Note: this is just for demonstrating the effect of any proper notification, real code should
As turned out by combined digging, it's strongly discouraged/disallowed to have nodes that are mutable in equals/hashcode:
But even with making both immutable (delegate to super in Person), the model still isn't working as expected - it seems impossible to modify it (f.i. insert a new Person) and make the tree update.
Code snippets to reproduce:
There is no immediate update of the tree, clicking on it later has unpredictable effects (f.i. showing a blank tree, similar as described in the question).
These can be fixed by changing the root to not return the list that describes the content: for some reason (that I don't fully understand, to be honest), having the list object as root confuses the tree.
Fixed model:
Aside: this adjustment ameliorates the problems with mutable hash/equals - but even then, we shouldn't :)