Preface
I know there are other tutorials about Drag and Drop with JTrees, but I felt that they were for the most part inadequate. The generally covered specific sections really well, but would go back to using "decrepit" methods for other sections, and didn't really give the user a full view of how it should be designed. So, hopefully this is a working compilation of all the examples I found. This has been implemented using JDK 6. Note: You must have JDK 6 (possible 7 when it gets released will work, too) because I make use of the TransferSupport class, a new class in SE 6. Also, note that this is a very long tutorial, as drag and drop is a pain in the you-know-what. This tutorial is aimed at people who have some knowledge of programming Swing GUI programming (mostly the API stuff) and have a good amount of knowledge programming in Java.
Specifications
Here's what you can do with this implementation of drag and drop:
1. Drag/drop multiple nodes at the same time (even only take parts of selections, but you'll have to implement that yourself).
2. Define how nodes get added/ don't get added.
3. Define how different actions can be treated.
4. Define copy or move of tree nodes.
5. Much much more (some how, this makes me sound like a salesman)
I chose to have most of the information stored inside of the code (in the form of comments and the actual code). Before each class, I also gave a brief description of what was required of each component.
JTree extension
In my implementation, I extended JTree so I can support some basic selection tracking in the tree. This isn't that big of a deal when there's only one node to keep track of, but for multiple selected nodes it's the easiest method. So, what we need in our DnDJtree is:
1. Implement TreeSelectionListener so we can keep track of selections.
2. Turn on drag and drop. All JComponents have the interface to handle drag and drop, but not all sub-classes implement the necessary components to make it work.
3. Setup a transfer handler. The transfer handler is a fairly intricate class that allows Java to transfer objects either to the system or to your program. It's used in both copy/paste and drag and drop. For our application, we'll only be focusing on the drag and drop portion of handling, but realize that the same infrastructure (and in fact, a lot of the same code) can be reused for copy/paste.
4. (optional) Setting the drop mode. I left the default drop mode which only allows you to drop onto the node, but you can change this to allow your DnDJTree to allow you to "insert" nodes into a location. Note that if you do choose this path, you will have to modify the JTreeTransferHandler because at the moment it's designed only to handle dropping onto a node.
The DnDJTree class:
import java.util.ArrayList; import javax.swing.JTree; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; /** * @author helloworld922 * <p> * @version 1.0 * <p> * copyright 2010 <br> * * You are welcome to use/modify this code for any purposes you want so * long as credit is given to me. */ public class DnDJTree extends JTree implements TreeSelectionListener { /** * */ private static final long serialVersionUID = -4260543969175732269L; /** * Used to keep track of selected nodes. This list is automatically updated. */ protected ArrayList<TreePath> selected; /** * Constructs a DnDJTree with root as the main node. * * @param root */ public DnDJTree(TreeNode root) { super(root); // turn on the JComponent dnd interface this.setDragEnabled(true); // setup our transfer handler this.setTransferHandler(new JTreeTransferHandler(this)); // setup tracking of selected nodes this.selected = new ArrayList<TreePath>(); this.addTreeSelectionListener(this); } /** * @return the list of selected nodes */ @SuppressWarnings("unchecked") public ArrayList<TreePath> getSelection() { return (ArrayList<TreePath>) (this.selected).clone(); } /** * keeps the list of selected nodes up to date. * * @param e **/ @Override public void valueChanged(TreeSelectionEvent e) { // should contain all the nodes who's state (selected/non selected) // changed TreePath[] selection = e.getPaths(); for (int i = 0; i < selection.length; i++) { // for each node who's state changed, either add or remove it form // the selected nodes if (e.isAddedPath(selection[i])) { // node was selected this.selected.add(selection[i]); } else { // node was de-selected this.selected.remove(selection[i]); } } } }
Transfer handler
From item 3 above, we added a transfer handler for our JTree. However, JTree's default transfer handler rejects all drops. Obviously, we need to change that. So, for our transfer handler we will need:
1. A method to check if data import is valid.
2. A method to add nodes to the appropriate drop location, and to remove nodes that were moved rather than copied.
For moving around nodes in a JTree it's best to do so with the tree model because it should handle the event generation/dispatch, as well as update the changes for us. I'm using the DefaultTreeModel because it works quite well.
Please note that because of the way I implemented tracking of selected nodes, it's possible that the nodes being dropped won't be in the same "order" as they were previously. ex:
root
-child1
-child2
-child3
(drop child2 and child3 into child1)
root
-child1
--child3
--child2
This is because the current implementation is based on the order they were queued into the selected queue. To change this, you need to either change the way selected nodes are queued or modify the importData method to take into account the current "order" of selected nodes.
JTreeTransferHandler class:
import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.io.IOException; import java.util.ArrayList; import javax.swing.*; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; /** * @author helloworld922 * <p> * @version 1.0 * <p> * copyright 2010 <br> * * You are welcome to use/modify this code for any purposes you want so * long as credit is given to me. */ public class JTreeTransferHandler extends TransferHandler { /** * Using tree models allows us to add/remove nodes from a tree and pass the * appropriate messages. */ protected DefaultTreeModel tree; /** * */ private static final long serialVersionUID = -6851440217837011463L; /** * Creates a JTreeTransferHandler to handle a certain tree. Note that this * constructor does NOT set this transfer handler to be that tree's transfer * handler, you must still add it manually. * * @param tree */ public JTreeTransferHandler(DnDJTree tree) { super(); this.tree = (DefaultTreeModel) tree.getModel(); } /** * * @param c * @return */ @Override public int getSourceActions(JComponent c) { return TransferHandler.COPY_OR_MOVE; } /** * * @param c * @return null if no nodes were selected, or this transfer handler was not * added to a DnDJTree. I don't think it's possible because of the * constructor layout, but one more layer of safety doesn't matter. */ @Override protected Transferable createTransferable(JComponent c) { if (c instanceof DnDJTree) { return new DnDTreeList(((DnDJTree) c).getSelection()); } else { return null; } } /** * @param c * @param t * @param action */ @Override protected void exportDone(JComponent c, Transferable t, int action) { if (action == TransferHandler.MOVE) { // we need to remove items imported from the appropriate source. try { // get back the list of items that were transfered ArrayList<TreePath> list = ((DnDTreeList) t .getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes(); for (int i = 0; i < list.size(); i++) { // remove them this.tree.removeNodeFromParent((DnDNode) list.get(i).getLastPathComponent()); } } catch (UnsupportedFlavorException exception) { // for debugging purposes (and to make the compiler happy). In // theory, this shouldn't be reached. exception.printStackTrace(); } catch (IOException exception) { // for debugging purposes (and to make the compiler happy). In // theory, this shouldn't be reached. exception.printStackTrace(); } } } /** * * @param supp * @return */ @Override public boolean canImport(TransferSupport supp) { // Setup so we can always see what it is we are dropping onto. supp.setShowDropLocation(true); if (supp.isDataFlavorSupported(DnDTreeList.DnDTreeList_FLAVOR)) { // at the moment, only allow us to import list of DnDNodes // Fetch the drop path TreePath dropPath = ((JTree.DropLocation) supp.getDropLocation()).getPath(); if (dropPath == null) { // Debugging a few anomalies with dropPath being null. In the // future hopefully this will get removed. System.out.println("Drop path somehow came out null"); return false; } // Determine whether we accept the location if (dropPath.getLastPathComponent() instanceof DnDNode) { // only allow us to drop onto a DnDNode try { // using the node-defined checker, see if that node will // accept // every selected node as a child. DnDNode parent = (DnDNode) dropPath.getLastPathComponent(); ArrayList<TreePath> list = ((DnDTreeList) supp.getTransferable() .getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes(); for (int i = 0; i < list.size(); i++) { if (parent.getAddIndex((DnDNode) list.get(i).getLastPathComponent()) < 0) { return false; } } return true; } catch (UnsupportedFlavorException exception) { // Don't allow dropping of other data types. As of right // now, // only DnDNode_FLAVOR and DnDTreeList_FLAVOR are supported. exception.printStackTrace(); } catch (IOException exception) { // to make the compiler happy. exception.printStackTrace(); } } } // something prevented this import from going forward return false; } /** * * @param supp * @return */ @Override public boolean importData(TransferSupport supp) { if (this.canImport(supp)) { try { // Fetch the data to transfer Transferable t = supp.getTransferable(); ArrayList<TreePath> list = ((DnDTreeList) t .getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes(); // Fetch the drop location TreePath loc = ((javax.swing.JTree.DropLocation) supp.getDropLocation()).getPath(); // Insert the data at this location for (int i = 0; i < list.size(); i++) { this.tree.insertNodeInto((DnDNode) list.get(i).getLastPathComponent(), (DnDNode) loc.getLastPathComponent(), ((DnDNode) loc .getLastPathComponent()).getAddIndex((DnDNode) list.get(i) .getLastPathComponent())); } // success! return true; } catch (UnsupportedFlavorException e) { // In theory, this shouldn't be reached because we already // checked to make sure imports were valid. e.printStackTrace(); } catch (IOException e) { // In theory, this shouldn't be reached because we already // checked to make sure imports were valid. e.printStackTrace(); } } // import isn't allowed at this time. return false; } }
DnDNode
The standard DefaultMutableTreeNode doesn't implement the Transferable interface, vital to use Java's framework for transfering data. Fortunately, it's not too hard to implement the interface. It's also very important that DnDNode be serializable. The default drag and drop framework requires that everything be serializable (including all the data inside). This was a common problem I ran into when I tried this on a custom object I created that wasn't serializable. The beauty of the Serializable interface, though, is that there are no methods you have to implement (makes me wonder why all objects aren't serializable).
Because the data I had was highly dependent on where it was in the tree (particularly what children it had) I designed this node to function so you could implement sub-classes and they would still function correctly.
DnDNode class:
package tree; import java.awt.datatransfer.*; import java.io.IOException; import java.io.Serializable; import javax.swing.tree.DefaultMutableTreeNode; /** * @author helloworld922 * <p> * @version 1.0 * <p> * copyright 2010 <br> * * You are welcome to use/modify this code for any purposes you want so * long as credit is given to me. */ public class DnDNode extends DefaultMutableTreeNode implements Transferable, Serializable { /** * */ private static final long serialVersionUID = 4816704492774592665L; /** * data flavor used to get back a DnDNode from data transfer */ public static final DataFlavor DnDNode_FLAVOR = new DataFlavor(DnDNode.class, "Drag and drop Node"); /** * list of all flavors that this DnDNode can be transfered as */ protected static DataFlavor[] flavors = { DnDNode.DnDNode_FLAVOR }; public DnDNode() { super(); } /** * * Constructs * * @param data */ public DnDNode(Serializable data) { super(data); } /** * Determines if we can add a certain node as a child of this node. * * @param node * @return */ public boolean canAdd(DnDNode node) { if (node != null) { if (!this.equals(node.getParent())) { if ((!this.equals(node))) { return true; } } } return false; } /** * Gets the index node should be inserted at to maintain sorted order. Also * performs checking to see if that node can be added to this node. By * default, DnDNode adds children at the end. * * @param node * @return the index to add at, or -1 if node can not be added */ public int getAddIndex(DnDNode node) { if (!this.canAdd(node)) { return -1; } return this.getChildCount(); } /** * Checks this node for equality with another node. To be equal, this node * and all of it's children must be equal. Note that the parent/ancestors do * not need to match at all. * * @param o * @return */ @Override public boolean equals(Object o) { if (o == null) { return false; } else if (!(o instanceof DnDNode)) { return false; } else if (!this.equalsNode((DnDNode) o)) { return false; } else if (this.getChildCount() != ((DnDNode) o).getChildCount()) { return false; } { // compare all children for (int i = 0; i < this.getChildCount(); i++) { if (!this.getChildAt(i).equals(((DnDNode) o).getChildAt(i))) { return false; } } // they are equal! return true; } } /** * Compares if this node is equal to another node. In this method, children * and ancestors are not taken into concideration. * * @param node * @return */ public boolean equalsNode(DnDNode node) { if (node != null) { if (this.getAllowsChildren() == node.getAllowsChildren()) { if (this.getUserObject() != null) { if (this.getUserObject().equals(node.getUserObject())) { return true; } } else { if (node.getUserObject() == null) { return true; } } } } return false; } /** * @param flavor * @return * @throws UnsupportedFlavorException * @throws IOException **/ @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (this.isDataFlavorSupported(flavor)) { return this; } else { throw new UnsupportedFlavorException(flavor); } } /** * @return **/ @Override public DataFlavor[] getTransferDataFlavors() { return DnDNode.flavors; } /** * @param flavor * @return **/ @Override public boolean isDataFlavorSupported(DataFlavor flavor) { DataFlavor[] flavs = this.getTransferDataFlavors(); for (int i = 0; i < flavs.length; i++) { if (flavs[i].equals(flavor)) { return true; } } return false; } /** * @param temp * @return */ public int indexOfNode(DnDNode node) { if (node == null) { throw new NullPointerException(); } else { for (int i = 0; i < this.getChildCount(); i++) { if (this.getChildAt(i).equals(node)) { return i; } } return -1; } } }
DnD nodes list
Unfortunately, I didn't find any standard collection classes that implemented transferable, but that's ok because our own implementation is very simple. Here's what we need:
1. Some sort of list to store nodes to transfer. i chose an ArrayList because I figured that this list will be accessed randomly a lot, but won't really be changed once it has been created.
2. Implementation of the Transferable interface. The implementation will look an awful lot like the DnDNodes.
DnDTreeList class:
import java.awt.datatransfer.*; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import javax.swing.tree.TreePath; /** * @author helloworld922 * <p> * @version 1.0 * <p> * copyright 2010 <br> * * You are welcome to use/modify this code for any purposes you want so * long as credit is given to me. */ public class DnDTreeList implements Transferable, Serializable { /** * */ private static final long serialVersionUID = 1270874212613332692L; /** * Data flavor that allows a DnDTreeList to be extracted from a transferable * object */ public final static DataFlavor DnDTreeList_FLAVOR = new DataFlavor(DnDTreeList.class, "Drag and drop list"); /** * List of flavors this DnDTreeList can be retrieved as. Currently only * supports DnDTreeList_FLAVOR */ protected static DataFlavor[] flavors = { DnDTreeList.DnDTreeList_FLAVOR }; /** * Nodes to transfer */ protected ArrayList<TreePath> nodes; /** * @param selection */ public DnDTreeList(ArrayList<TreePath> nodes) { this.nodes = nodes; } public ArrayList<TreePath> getNodes() { return this.nodes; } /** * @param flavor * @return * @throws UnsupportedFlavorException * @throws IOException **/ @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (this.isDataFlavorSupported(flavor)) { return this; } else { throw new UnsupportedFlavorException(flavor); } } /** * @return **/ @Override public DataFlavor[] getTransferDataFlavors() { // TODO Auto-generated method stub return DnDTreeList.flavors; } /** * @param flavor * @return **/ @Override public boolean isDataFlavorSupported(DataFlavor flavor) { DataFlavor[] flavs = this.getTransferDataFlavors(); for (int i = 0; i < flavs.length; i++) { if (flavs[i].equals(flavor)) { return true; } } return false; } }
Conclusion
That's pretty much all there is to implementing a "simple" drag and drop for JTrees that allows multiple node trasfers. If you want, here's a small test application that sets up a DnDJTree with some nodes.
public class DnDJTreeApp { public static void main(String[] args) { DnDNode root = new DnDNode("root"); DnDNode child = new DnDNode("parent 1"); root.add(child); child = new DnDNode("parent 2"); root.add(child); child = new DnDNode("parent 3"); child.add(new DnDNode("child 1")); child.add(new DnDNode("child 2")); root.add(child); DnDJTree tree = new DnDJTree(root); JFrame frame = new JFrame("Drag and drop JTrees"); frame.getContentPane().add(tree); frame.setSize(600, 400); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }
Happy coding
edit: made some minor changes to DnDNode so it supports null user objects correctly now.