HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML

The ability to transfer information by dragging data from one component to another has been around since the development of the graphical user interface. Over the years drag-and-drop has gone from a cool feature to a required piece of most user interfaces. Most users expect to be able to drag objects between fields, windows, or folders and have some action occur. Drag-and-drop is even used to open applications by dragging a file to an application icon.

For a Java developer creating user interfaces it's no longer a question of whether drag-and-drop should be used but of how much. Java provides a set of classes for implementing the drag-and-drop interface. While it's not overly complex, implementing it in a complex GUI with a number of drag sources and drop targets can be tedious and error-prone. In this article, I develop an abstract DnDHandler class that takes care of most of the tedium and simplifies the implementation of drag-and-drop.

Drag-and-Drop Review
Although there are many parts to a drag-and-drop transaction, it can basically be broken down into three main components (see Figure 1):
1. Starting the drag where the drag action is recognized by the component
2. Converting the drag item into a transferable data type
3. Dropping the transferable data into the drop target

Figure 1
Figure  1:

During the drag-and-drop transaction many other minor parts allow for user feedback, but these won't be discussed in any detail in this article. For more information on the details of drag-and-drop, see the reference at the end of the article.

Consider the simple drag-and-drop application shown in Figure 2 that contains two components: a DraggableTree on the left and a DroppableList on the right. The user can select items from the tree and drag them into the list.

Figure 2
Figure  2:

The code for the DraggableTree is shown in Listing 1 and the code for the DroppableList is shown in Listing 2. Listings 1-5 can be downloaded from below.

To make the tree a draggable component a number of things must be done. First, the tree must implement the DragGestureListener interface, then create an instance of a DragSource and call the createDefaultDragGestureRecognizer method so that the tree will be notified when a drag action has been initiated.

When a drag-and-drop action occurs, the dragGestureRecognized method is called. This method first checks to see if something has been selected. If it has, the method then gets the selected object, creates a transferable version of the data, and calls the start drag function.

The list that acts as the drop target must implement the DropTargetListener interface and create a DropTarget instance. The DropTarget constructor is used to notify the drag-and-drop framework that the list will accept dragged objects.

Although a number of methods are defined in the DropTargetListener interface, the main one is the drop method, which is called when an object is dropped into the list.

The drop method gets the transferable data from the DropTargetDropEvent. If the transferable data is the right data type, a number of steps are taken. First, the drop is accepted. Next, the transferred data is retrieved and the item is added to the list. Last, the dropComplete function is called to notify the drag-and-drop framework that the drop was completed successfully.

There's a lot more to drag-and-drop than presented in this simple example, but the example shows the basic steps in any drag-and-drop transaction.

The DNDHandler Class
Unlike the previous example, implementing drag-and-drop in a more complex GUI or a multiple document interface involves a lot of code duplication, if there are more than a couple of drag-and-drop targets. It's better if all the common code for a drag-and-drop transaction is contained in a single class where it's reused by any GUI component that needs to implement drag-and-drop.

This common drag-and-drop class is called DnDHandler. It should contain as much of the drag-and-drop functionality as possible and implement both the drag and the drop interfaces.

The requirements needed to create a draggable GUI component are:

  • Implement the DragGestureListener interface
  • Create an instance of a DragSource
  • Call the createDefaultDragGestureRecognizer method so the component will be notified when a drag action has been initiated
  • Create a dragGestureRecognized method that will get the draggable data and start the drag
The only problem with making these requirements into a generic class is that the dragGestureRecognized method must know how to get data from the actual component. To get around this problem, the dragGestureRecognized in the DnDHandler class is changed so it calls a getTransferable method to get the data from the actual component. The DnDHandler class makes this an abstract method so it will need to be implemented by the class extending it.

Now let's look at the requirements for a droppable GUI component:

  • Implement the DropTargetListener interface
  • Create an instance of a DropTarget instance
  • Create a drop method to add the dragged data to the component
Again, the only problem with making these requirements generic is the drop method, which needs to know how to add the data to the dropped component. For the DnDHandler class we'll modify the drop method to call an abstract handleDrop method that needs to be implemented by the class extending the DnDHandler class. The resulting DnDHandler class is shown in Listing 3.

The DnDHandler class implements the DropTargetListener, DragSourceListener, and DragGestureListener interfaces. Its constructor takes the components as an argument and creates an instance of a DragSource and DropTarget and contains three abstract methods:

  1. getTransferable: Gets transferable data from the component
  2. handleDrop: Handles the drop action
  3. getSupportedDataFlavors: Gets the transferable data that's supported
This DnDHandler class encapsulates all the requirements needed to create a drag-and-drop interface.

Using the DnDHandler Class
Let's now rework our original drag-and-drop example using the DnD- Handler class and see how this simplifies the implementation. To use the DnDHandler class, the new class that's used to replace DraggableTree needs to extend both JTree and the DnDHandler classes, but Java's single inheritance limitation makes this impossible. To get around this limitation we use an inner class to extend the DnDHandler class, while the main class extends JTree.

The reworked DraggableTree class, DNDTree, is shown in Listing 4. The inner class DNDTreeHandler does most of the work. The only thing the outer class does is create an instance of the DNDTreeHandler. The DNDTreeHandler class has the one argument constructor needed by its parent and the implementation of the three abstract methods.

By implementing the handleDrop, the DNDTree class has some improved functionality over the original version. The tree is now a drop target and items from the list can be dragged onto the tree. This is one of the advantages of using the DNDHandler class: instead of implementing all the methods of a DropTargetListener, we simply write the handleDrop function. If we didn't want the DNDTree to be a drop target, we would still have to implement the handleDrop function, but we could just ignore the drop event or call the rejectDrop method.

The reworked Droppable list class is called DNDList (see Listing 5). It's coded in a similar manner to the DNDTree class.

The reworked example is still a simple application of drag-and-drop but it does show how the DNDHandler calls simplify drag-and-drop implementation. Instead of implementing three interfaces and numerous methods, drag-and-drop can now be executed by writing three simple methods.

Conclusion
The DnDHandler class has proven very useful in our development of user interfaces. It allows us to add drag-and-drop features to the user interface more quickly and also provides a central place to control the low-level workings of drag-and-drop. We're using it under Java 1.3 on both Windows and UNIX and haven't experienced any problems.

Most real-world projects will have many more transferable data types than the simple example presented. The management of these transferable data types is an important implementation issue that must be considered when using drag-and-drop in a complex user interface.

Hopefully this article has taken some of the mystery out of implementing drag-and-drop. Although it looks difficult, it's fairly straightforward using a class such as DnDHandler.

Reference
1. Zukowski, J. (1999). John Zukowski's Definitive Guide to Swing for Java 2. Apress.

Author Bio
Thomas Hammell is a senior developer at Hewlett-Packard Bluestone and is part of the tools groups that develops various tools for HP middleware products. He has over 15 years of experience in developing software. Tom holds a BS in electrical engineering and an MS in computer science from Stevens Institute of Technology. [email protected]

	



Listing 1 - Draggable Tree1

import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.dnd.*;
import java.io.IOException;

public class DraggableTree extends JTree implements DragGestureListener
{
  DragSource dragSource = DragSource.getDefaultDragSource();

  final static DragSourceListener dragSourceListener = new theDragSourceListener();

  public DraggableTree ()
  {
    dragSource.createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY_OR_MOVE, this);
    setAutoscrolls(true);
  }

  // DragGestureListener

  public void dragGestureRecognized(DragGestureEvent dragGestureEvent)
  {
    TreePath path = getSelectionPath();
    if (path == null)
    {
      // Nothing selected, nothing to drag
      System.out.println ("Nothing selected - beep");
      getToolkit().beep();
    }
    else
    {
      DefaultMutableTreeNode selection = (DefaultMutableTreeNode)path.getLastPathComponent();
      TransferableDataItem node = new TransferableDataItem(selection);
      dragSource.startDrag(dragGestureEvent, DragSource.DefaultCopyDrop, node, dragSourceListener);
    }
  }

  static class theDragSourceListener implements DragSourceListener
  {
    public void dragDropEnd(DragSourceDropEvent dragSourceDropEvent)
    {
    }
    public void dragEnter(DragSourceDragEvent dragSourceDragEvent)
    {
    }
    public void dragExit(DragSourceEvent dragSourceEvent)
    {
    }
    public void dragOver(DragSourceDragEvent dragSourceDragEvent)
    {
    }
    public void dropActionChanged(DragSourceDragEvent dragSourceDragEvent)
    {
    }
  }

}

Listing 2 - Droppable List1

import java.awt.*;
import java.awt.dnd.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import java.io.*;
import java.util.*;
import java.util.List;

public class DroppableList extends JList implements DropTargetListener
{

  DropTarget dropTarget;

  public DroppableList()
  {
    dropTarget = new DropTarget (this, this);
    setModel(new DefaultListModel());
  }

  public void dragEnter (DropTargetDragEvent dropTargetDragEvent)
  {
    dropTargetDragEvent.acceptDrag (DnDConstants.ACTION_COPY_OR_MOVE);
  }

  public void dragExit (DropTargetEvent dropTargetEvent)
  {
  }

  public void dragOver (DropTargetDragEvent dropTargetDragEvent)
  {
  }

  public void dropActionChanged (DropTargetDragEvent dropTargetDragEvent)
  {
  }

  public synchronized void drop (DropTargetDropEvent dropTargetDropEvent)
  {
    Point location = dropTargetDropEvent.getLocation();
    try
    {
     Transferable tr = dropTargetDropEvent.getTransferable();
      if (tr.isDataFlavorSupported(TransferableDataItem.DEFAULT_MUTABLE_DATAITEM_FLAVOR))
      {
        dropTargetDropEvent.acceptDrop (DnDConstants.ACTION_COPY_OR_MOVE);
        Object userObject = tr.getTransferData(TransferableDataItem.DEFAULT_MUTABLE_DATAITEM_FLAVOR);
        addElement(location, userObject);
        dropTargetDropEvent.getDropTargetContext().dropComplete(true);
      }
      else
      {
        System.err.println ("Rejected");
        dropTargetDropEvent.rejectDrop();
      }
    }
    catch (IOException io)
    {
      io.printStackTrace();
      dropTargetDropEvent.rejectDrop();
    }
    catch (UnsupportedFlavorException ufe)
    {
      ufe.printStackTrace();
      dropTargetDropEvent.rejectDrop();
    }
  }
  private void addElement(Point location, Object element) {
    int index = locationToIndex(location);
    // If index not found, add at end, otherwise add one beyond position
    if (index == -1) {
      index = getModel().getSize();
    }
    else
    {
      index++;
    }
    ((DefaultListModel)getModel()).add(index, element);
  }
}

Listing 3 - DnDHandler1


import java.awt.dnd.*;
import java.awt.datatransfer.*;
import java.awt.Cursor;
import java.awt.Component;

public abstract class DnDHandler implements DropTargetListener,DragSourceListener, DragGestureListener
{

  protected DropTarget dropTarget;
  protected DragSource dragSource;

  public DnDHandler(Component dndComponent)
  {
    dropTarget = new DropTarget (dndComponent, this);
    dragSource = new DragSource();
    dragSource.createDefaultDragGestureRecognizer( dndComponent, DnDConstants.ACTION_COPY_OR_MOVE, this);
  }


  //creates transferable based on what's selected or returns null if nothings selected
  protected abstract Transferable getTransferable();
  protected abstract void handleDrop(Transferable transferable, DropTargetDropEvent event) throws Exception;
  protected abstract DataFlavor[] getSupportedDataFlavors();

  public void dropFailed (DragSourceDropEvent event)
  {
     System.out.println("Drop Failed");
  }

  public void dropSuccess (DragSourceDropEvent event)
  {
     System.out.println("Drop was successful");
  }

  private boolean isTransferableSupported(Transferable t)
  {
      DataFlavor[] flavors = getSupportedDataFlavors();
      for (int i=0; i<flavors.length; i++)
      {
          if (t.isDataFlavorSupported(flavors[i]) )
              return true;
      }
      return false;
  }


  public void dragGestureRecognized( DragGestureEvent event)
  {

        Transferable trans = getTransferable();
        Cursor dragIcon = getDragCursor(event);

        if (trans != null)
        {
            // Starts the dragging
            dragSource.startDrag (event, dragIcon, trans, this);
        }
        else
        {
            System.out.println( "nothing was selected");
        }
  }

  /**
   * a drop has occurred
   *
   */
  public void drop (DropTargetDropEvent event)
  {

    try
    {
        Transferable transferable = event.getTransferable();

        // we accept only Strings
        if (isTransferableSupported (transferable))
        {
            event.acceptDrop(event.getDropAction());
            handleDrop(transferable, event);
            event.getDropTargetContext().dropComplete(true);
        }
        else
        {
            event.rejectDrop();
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
        System.err.println( "Drop Exception" + e.getMessage());
        event.rejectDrop();
    }

  }

  //DragSourceListener interfaces
  /**
   * is invoked when you are dragging over the DropSite
   *
   */
  public void dragEnter (DropTargetDragEvent event)
  {

    // debug messages for diagnostics
    //System.out.println( "dragEnter");

    int action = event.getDropAction();
    event.acceptDrag (action);

  }

  /**
   * is invoked when you are exit the DropSite without dropping
   *
   */
  public void dragExit (DropTargetEvent event)
  {
    //System.out.println( "dragExit");
  }

  /**
   * is invoked when a drag operation is going on
   *
   */
  public void dragOver (DropTargetDragEvent event)
  {
    //System.out.println( "dragOver");
  }

  /**
   * is invoked if the use modifies the current drop gesture
   *
   */


  public void dropActionChanged ( DropTargetDragEvent event )
  {
  }



  //gets the cursor to start drag
  protected Cursor getDragCursor( DragGestureEvent event)
  {
      if (event.getDragAction() == DnDConstants.ACTION_MOVE)
      {
          return DragSource.DefaultMoveDrop;
      }
      if (event.getDragAction() == DnDConstants.ACTION_COPY_OR_MOVE)
      {
          return DragSource.DefaultCopyDrop;
      }
      else
      {
          return Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
      }
  }


  /**
   * this message goes to DragSourceListener, informing it that the dragging
   * has ended
   *
   */

  public void dragDropEnd (DragSourceDropEvent event) {
    if ( event.getDropSuccess())
    {
        dropSuccess(event);
    }
    else
    {
        dropFailed(event);
    }
  }

  /**
   * this message goes to DragSourceListener, informing it that the dragging
   * has entered the DropSite
   *
   */
  public void dragEnter (DragSourceDragEvent event)
  {
    //System.out.println( " dragEnter");
  }

  /**
   * this message goes to DragSourceListener, informing it that the dragging
   * has exited the DropSite
   *
   */
  public void dragExit (DragSourceEvent event)
  {
    //System.out.println( "dragExit");

  }

  /**
   * this message goes to DragSourceListener, informing it that the dragging is currently
   * ocurring over the DropSite
   *
   */
  public void dragOver (DragSourceDragEvent event)
  {
    //System.out.println( "dragExit");

  }

  /**
   * is invoked when the user changes the dropAction
   *
   */
  public void dropActionChanged ( DragSourceDragEvent event)
  {
    //System.out.println( "dropActionChanged");
  }

}

Listing 4 - DNDTree1

import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.dnd.*;
import java.awt.dnd.DnDConstants;
import java.io.IOException;
import java.awt.datatransfer.*;

/**
 * Title:
 * Description:
 * Copyright:    Copyright (c)
 * Company:
 * @author
 * @version 1.0
 */

public class DNDTree extends JTree
{
  public DNDTree ()
  {
    DNDTreeHandler dndHandler = new DNDTreeHandler(this);

    setAutoscrolls(true);
  }

  public class DNDTreeHandler extends DnDHandler
  {
     TransferableDataItem transDataItem = new TransferableDataItem();

     public DNDTreeHandler(Component dndComponent)
     {
        super(dndComponent);
     }
     public DataFlavor[] getSupportedDataFlavors()
     {
        return transDataItem.getTransferDataFlavors();
     }


    /**
     * Gets the data from the selected object being dragged
     *
     * @return node DataItem being dragged
     */
    public Transferable getTransferable()
    {
       TreePath path = getSelectionPath();
       DefaultMutableTreeNode selection = (DefaultMutableTreeNode)path.getLastPathComponent();
       if (path == null)
       {
          return(null);
       }
       else
       {
         TransferableDataItem node = new TransferableDataItem(selection);
         return(node);
       }
     }

     /**
      * Handles the drop of the component after the drag is complete
      *
      * @param transferable Object being dropped
      * @param event drop event information
      */
     public void handleDrop(Transferable transferable, DropTargetDropEvent event) throws Exception
     {
           Point location = event.getLocation();
           //Get path of node where object being dropped
           TreePath path = getClosestPathForLocation(location.x, location.y);
           //No drop target
           if(path == null)
           {
              System.err.println ("Rejected");
              event.rejectDrop();
           }
           else
           {
              Transferable tr = event.getTransferable();

              if(tr.isDataFlavorSupported(TransferableDataItem.DEFAULT_MUTABLE_DATAITEM_FLAVOR))
              {
                 System.out.println("Got transfered data");
                 Object userObject = tr.getTransferData(TransferableDataItem.DEFAULT_MUTABLE_DATAITEM_FLAVOR);
                 DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
                 DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(userObject.toString());
                 DefaultTreeModel model = (DefaultTreeModel)getModel();
                 model.insertNodeInto(newNode, node, 0);
              }
              else
              {
                 System.err.println ("Rejected");
                 event.rejectDrop();
              }
           }

     }
   }
}

Listing 5 - DNDList1

import javax.swing.JList;
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.dnd.*;
import java.awt.dnd.DnDConstants;
import java.io.IOException;
import java.awt.datatransfer.*;

public class DNDList extends JList
{
  DropTarget dropTarget;

  public DNDList()
  {
    DNDListHandler dndHandler = new DNDListHandler(this);

    setModel(new DefaultListModel());
  }

  private void addElement(Point location, Object element)
  {
    int index = locationToIndex(location);
    // If index not found, add at end, otherwise add one beyond position
    if (index == -1) {
      index = getModel().getSize();
    }
    else
    {
      index++;
    }
    ((DefaultListModel)getModel()).add(index, element);
  }

  public class DNDListHandler extends DnDHandler
  {
     TransferableDataItem transDataItem = new TransferableDataItem();

     public DNDListHandler(Component dndComponent)
     {
        super(dndComponent);
     }
     public DataFlavor[] getSupportedDataFlavors()
     {
        return transDataItem.getTransferDataFlavors();
     }
     public DataFlavor[] getTransferDataFlavors()
     {
       return transDataItem.getTransferDataFlavors();
     }

    /**
     * Gets the data from the selected object being dragged
     *
     * @return node Node being dragged
     */
    public Transferable getTransferable()
    {
       Object selectedItem = getSelectedValue();
       System.out.println("Selected Value is " + selectedItem);
       TransferableDataItem item = new TransferableDataItem(selectedItem);
       return(item);
    }
     /**
      * Handles the drop of the component after the drag is complete
      *
      * @param transferable Object being dropped
      * @param event drop event information
      */
     public void handleDrop(Transferable transferable, DropTargetDropEvent event) throws Exception
     {
           Transferable tr = event.getTransferable();
           Point location = event.getLocation();
           if (tr.isDataFlavorSupported(TransferableDataItem.DEFAULT_MUTABLE_DATAITEM_FLAVOR))
           {
              //event.acceptDrop (DnDConstants.ACTION_COPY_OR_MOVE);
              Object userObject = tr.getTransferData(TransferableDataItem.DEFAULT_MUTABLE_DATAITEM_FLAVOR);
              addElement(location, userObject);
             //dropTargetDropEvent.getDropTargetContext().dropComplete(true);
           }
           else
           {
              System.err.println ("Rejected");
              event.rejectDrop();
           }
      }
   }
}



import javax.swing.tree.*;
import java.awt.dnd.*;
import java.awt.datatransfer.*;
import java.io.*;


public class TransferableDataItem extends DefaultMutableTreeNode implements Transferable
{

  final static int DATA_ITEM = 0;
  final static int STRING = 1;
  final static int PLAIN_TEXT = 2;

  final public static DataFlavor DEFAULT_MUTABLE_DATAITEM_FLAVOR =
    new DataFlavor(DefaultMutableTreeNode.class, "Default Mutable Data Item");

  static DataFlavor flavors[] = {DEFAULT_MUTABLE_DATAITEM_FLAVOR, DataFlavor.stringFlavor, DataFlavor.plainTextFlavor};

  private Object data;

  public TransferableDataItem()
  {
  }
  public TransferableDataItem(Object data)
  {
    this.data = data;
  }

  public DataFlavor[] getTransferDataFlavors()
  {
    return flavors;
  }

  public Object getTransferData(DataFlavor flavor)
      throws UnsupportedFlavorException, IOException
  {
    Object returnObject;
    if (flavor.equals(flavors[DATA_ITEM]))
    {
        returnObject = data;
    }
    else if (flavor.equals(flavors[STRING]))
    {

        returnObject = data.toString();
    }
    else if (flavor.equals(flavors[PLAIN_TEXT]))
    {
        returnObject = new ByteArrayInputStream(data.toString().getBytes());
    }
    else
    {
      throw new UnsupportedFlavorException(flavor);
    }
    return returnObject;
  }
  public boolean isDataFlavorSupported(DataFlavor flavor)
  {
    boolean returnValue = false;
    for (int i=0, n=flavors.length; i<n; i++) {
      if (flavor.equals(flavors[i]))
      {
        returnValue = true;
        break;
      }
    }
    return returnValue;
  }
}

  
 

Download Additional Source Files (~ 32.2 KB ~Zip File Format)

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.