Overview
It seems obvIoUs enough: You have an XML document or fragment. XML is hierarchical. A Swing JTree displays hierarchical data. How do you make a JTree display your XML fragment?
If you understand that Swing's architecture uses MVC,you probably know you need a "model" that your JTree instance can be instructed to use. However,the only real concrete model class in the standard Swing API is the DefaultTableModel class. This class provides objects to the tree that implement the TreeNode interface. If you have started down this path,subclassing and customizing the standard behavior of the DefaultTableModel and working with your own DefaultTreeNode objects just to display XML will quickly give you a headache.
About this Article
I will be staying completely within the standard API,so no third-party XML libraries will be needed. If you need to get more familliar with the XML classes in the standard API,you might want to read one of my earlier articles "Working with XML and Java" or consult your favorite search engine. I also tend to use Java 5 Syntax (generics and enhanced-for). The reader is assumed to be somewhat familiar with Swing.
The TreeModel Interface
This interface defines the following methods. I have borrowed the descriptions directly from the Java API documentation.
- @H_403_52@void addTreeModelListener(TreeModelListener l): Adds a listener for the TreeModelEvent posted after the tree changes.
- @H_403_52@void removeTreeModelListener(TreeModelListener l): Removes a listener prevIoUsly added with addTreeModelListener.
- @H_403_52@Object getRoot()Returns the root of the tree.
- @H_403_52@int getChildCount(Object parent): Returns the number of children of parent.
- @H_403_52@Object getChild(Object parent,int index): Returns the child of parent at index index in the parent's child array.
- @H_403_52@boolean isLeaf(Object node): Returns true if node is a leaf.
- @H_403_52@int getIndexOfChild(Object parent,Object child): Returns the index of child in parent.
- @H_403_52@void valueForPathChanged(TreePath path,Object newValue): Messaged when the user has altered the value for the item identified by path to newValue.
I have listed the methods roughly in order of usage. When a JTree is given a TreeModel implementation to use,it registers itself as a TreeModelListener. (A good model implementation will alert all of its listeners if the structure of the tree changes,or if a node value changes. This lets the tree know it needs to redraw itself.) The root node is consulted first,and then the children for each node are obtained to build up the display. The icon in the tree that appears to each entry is determined from the result of whether that entry is a leaf or not. (Generally,if a node returns '0' for getChildCount,it should return 'true' for isLeaf... but,this is not set in stone.) Finally,if the values in the model are mutable (they can be edited) and the tree is editable,the tree will communicate with the edited value with the model by way of the valueForPathChanged method. (This example won't use an editable tree,so you won't implement this last method.)
An important point to note is that,being an interface,the TreeModel class doesn't concern itself with exactlywherethe data comes from or how it is stored. Because you will be dealing with an XML document,however,your implementation will include an instance field for an org.w3c.dom.Document class,and a corresponding getter/setter method pair. The TreeModel interface methods will simply work off of the Document object directly:
protected Document document; public Document getDocument() { return document; } public void setDocument(Document doc) { this.document = doc; TreeModelEvent evt = new TreeModelEvent(this,new TreePath(getRoot())); for (TreeModelListener listener : listeners) { listener.treeStructureChanged(evt); } }
Any time you alter the data model—such as when you replace the source XML document completely in the setDocument method—you need to alert all the listeners of this fact. This is what the extra code in setDocument takes care of. (The definition of the "listeners" variable will be introduced shortly. Bear with me.)
Before you start writing the TreeModel method implementations,you should carefully note that the model returns back objects of type java.lang.Object to the tree (for the root node and all children underneath it). The tree,by way of its default TreeCellRenderer,neither knows nor cares exactly what class these objects truly are. To know what label to draw in the tree for any given node,the toString method is called. (A special-purpose,highly customized tree might use a different tree cell renderer,which might be written to use something other than 'toString' to generate the node labels ... but that is beyond the scope of this article.) With this being the case,the standard org.w3c.dom.Node class doesn't provide a terribly useful return value for the toString method... at least not that useful to the casual end-user. To address this,you will wrap the Element objects in a custom class that provides a more intelligent response to the toString method: It will return the name of the Element using the getNodeName method. Look at this wrapper class first:
public class XMLTreeNode { Element element; public XMLTreeNode(Element element) { this.element = element; } public Element getElement() { return element; } public String toString() { return element.getNodeName(); } }
Another minor thing to nail down relates to the nature of "pretty-printed" XML and the return value of a Node object's getChildNodes() method. (You might want to read my prevIoUs article that discusses this. The link is in the Overview section.) My design requirement is that I only want to show actual Element objects in the tree,not text nodes,comments,attributes,or other non-Element node types. To address this,a helper method is written to return a Vector of all the children of a given node that are specifically of the Element type:
private Vector<Element> getChildElements(Node node) { Vector<Element> elements = new Vector<Element>(); NodeList list = node.getChildNodes(); for (int i=0 ; i<list.getLength() ; i++) { if (list.item(i).getNodeType() == Node.ELEMENT_NODE) { elements.add( (Element) list.item(i)); } } return elements; }
You will see this method used in several of the implementation methods,which you can finally address. The first item of business is to keep track of the registered TreeModelListeners:
Vector<TreeModelListener> listeners = new Vector<TreeModelListener>(); public void addTreeModelListener(TreeModelListener listener) { if (!listeners.contains(listener)) { listeners.add(listener); } } public void removeTreeModelListener(TreeModelListener listener) { listeners.remove(listener); }
For the next item,you need to provide the root node for the tree. This will be the root node of our document object,wrapped in the XMLTreeNode explained above:
public Object getRoot() { if (document==null) { return null; } Vector<Element> elements = getChildElements(document); if (elements.size() > 0) { return new XMLTreeNode( elements.get(0)); } else { return null; } }
Returning null is completely fine,and signals to the JTree object that there is nothing to be displayed. A quick safety check allows the JTree to function during initialization before the model is handed a Document to work with. (Failing to do this will generate NullPointerExceptions.)
Now that the tree has the root node,it will start asking for the number of child nodes under each node,and will ask for each of those child nodes in turn. The returned children objects will again be Element objects wrapped in an XMLTreeNode object.
public int getChildCount(Object parent) { if (parent instanceof XMLTreeNode) { Vector<Element> elements = getChildElements( ((XMLTreeNode)parent).getElement() ); return elements.size(); } return 0; } public Object getChild(Object parent,int index) { if (parent instanceof XMLTreeNode) { Vector<Element> elements = getChildElements( ((XMLTreeNode)parent).getElement() ); return new XMLTreeNode( elements.get(index) ); } else { return null; } } public int getIndexOfChild(Object parent,Object child) { if (parent instanceof XMLTreeNode && child instanceof XMLTreeNode) { Element pElement = ((XMLTreeNode)parent).getElement(); Element cElement = ((XMLTreeNode)child).getElement(); if (cElement.getParentNode() != pElement) { return -1; } Vector<Element> elements = getChildElements(pElement); return elements.indexOf(cElement); } return -1; }
Now,to inform the default table cell renderer which icon to draw for a given node,the follow method is consulted:
public boolean isLeaf(Object node) { if (node instanceof XMLTreeNode) { Element element = ((XMLTreeNode)node).getElement(); Vector<Element> elements = getChildElements(element); return elements.size()==0; } else { return true; } }
The final method is not needed for the purposes of the demo because you are not allowing edits to be made to the tree. Therefore,a dummy implementation is given:
public void valueForPathChanged(TreePath path,Object newValue) { throw new UnsupportedOperationException(); }
That's everything you need. The rest just involves a little code to create a demo interface with a JTree in it,to load an XML-formatted file into a Document object,and to pass this off to an XMLTreeModel instance. I will leave most of these details for the reader to experiment with,but will offer at least one parting screenshot. Consider the following XML document:
<?xml version="1.0"?> <root> <settings>This is a test.</settings> <body> <title>My Title</title> <info>The quick brown fox jumps over the lazy dog.</info> </body> </root>
An interface that includes the JTree itself (and also includes code that listens for user selections of nodes in the tree to display text content into a standard JTextField) is available in thedownloadable code bundle. The screenshot on a Mac system looks as follows: