Channels ▼
RSS

JVM Languages

How Does Java Drag-and-Drop Work?

Source Code Accompanies This Article. Download It Now.


Nov98: Java Q&A

Jason is a product manager for Stingray Software, a division of Rogue Wave Software, as well as a Math/Science senior at the University of North Carolina at Chapel Hill. He can be contacted at jason@ stingray.com.


Implementing drag-and-drop (DnD) in Java projects can mean different things. If the whole DnD process is going to be contained within the same instance of a component, then you can implement DnD by adding it within the event handlers of your component and defining your painting methods to handle the different states and their transitions accordingly. The good news is that this has always been available with the JDK and you can implement this today. The bad news is that most people want to go outside the component, to drag or drop from another component, Java app, or even an OS-native application. Until the advent of JavaSoft's Java Foundation Classes (JFC) and Microsoft's Windows Foundation Classes (WFC), these needs have been left unfulfilled.

JFC and WFC introduce DnD capabilities that make it possible to drag or drop outside the box. In this article, I'll examine what JFC and WFC mean in terms of drag-and-drop. This article is based on JFC from JDK 1.2b3 and WFC from Visual J++ Tech. Preview 1.

How it Works

Of the two, JFC's DnD process is the more complicated. This is probably because JFC has to serve all platforms, while WFC (which is targeted specifically for Windows) can make a lot of shortcuts and assumptions that JFC cannot. In the end, though, you will be rewarded with a DnD process that can be enabled on any platform supporting a JVM. So how does it work? First, a component needs to be defined as a DnD initiator, which calls upon the JVM to initiate the DnD process. The JVM then picks up the DnD process and takes care of the users' events as they drag the cursor around, until they release the mouse to instigate the drop/finale of the DnD process. The end component is the DropTarget, something that can receive the drop. In between the drag initiation and the final drop, the JVM dispatches various drag events to different listeners. It's important to note that the DnD initiator and the DropTarget can be the same component; that is, you can have a ListBox do both in an app with two instances of the ListBox, so users can drag from one ListBox to another.

Listing One (at the end of this article) is an example DnD initiator. As you can see, it looks like a normal component except that it implements DragSourceListener and has additional code in the processMouseEvent handler method. Any DnD initiator must have an associated DragSourceListener, which will be passed to the JVM upon the initiation such that the JVM will dispatch various DragSource events during the DnD process. DragSourceListener, of course, can be the same class as the DnD initiator component as is the example in Listing One, or you can handle the interface in a separate class.

The next step is setting up and initiating the drag. Most instances of a DnD initiation are going to be from user events, but you can also initiate a DnD process programmatically. The correct user event needs to be determined and handled appropriately. For example, pressing or dragging the mouse within a component would be a good indicator for a DnD process initiation. So you would add the DnD initiation code into the MOUSE_PRESSED or MOUSE_DRAGGED MouseEvent handlers. However, you should also be able to handle it if a user changes his mind and releases the mouse while also in the same component, so you would need to add DnD termination code into the MOUSE_RELEASED MouseEvent handler.

When you're ready to initiate the DnD process, you obtain a DragSource instance by calling DragSource's static getDefaultDragSource() method, figure out what type of data you're going to transfer, and then call the DragSource's startDrag() method with all of its parameters. The data type that's being transferred can be any type you want, though there's prebuilt support for Strings with the StringSelection class. The data types can be customized, however, by implementing the Transferable interface (available electronically; see "Resource Center," page 3).

The startDrag() method is pretty demanding with all the parameters it wants, but it is neat how customizable you can get with the whole DnD process. The first two parameters are pretty simple: the component that the DnD initiation occurred in (usually "this") and the AWTEvent that triggered the initiation (if this is being done within an event handler, you can simply refer to the handled event). The next two parameters specify the DnD action (int constants from the DnDConstants class) and the associated cursor that's being shown throughout the process. The DragSource class contains six static cursors that can be used, but you can also specify your own. Usually, the type of action your DnD process will undertake can be determined from the trigger event (for instance, did the user hold down the Shift key while initiating the drag?). The middle parameters are optional ways to customize the DnD process even further: An Image to show, and a Point which specifies how offset from the cursor to show the Image, throughout the DnD process. If this type of customization is not desirable, you can specify "null" for both arguments. The last two parameters simply specify the Transferable instance that's going to be transferred and the DragSourceListener that the DnD process will dispatch events to.

Developing DropTarget

That's the JFC side of the whole DnD process. You can stop there, if you're sure that there are components available which support your Transferable object when it's dropped. However, programmers usually develop components in tandem with each other. Developing your DnD counterpart (the DropTarget), though, is a little tricky. Listing Two presents an example that complements Listing One.

First, a few show-stopping caveats: The DropTarget must be a component that calls the Component's setDropTarget() method in its constructor, and the DropTarget must be in an application or trusted environment to receive drops. The DropTarget must also implement the DropTargetListener interface. From the interface, the minimalist approach is to provide a definition for the drop() method. The rest of the interface methods are good for providing even more feedback to the user as they proceed through the DnD process. The drop() method must undergo a precise order to obtain the final result.

The first thing you should do is obtain the Transferable instance from the event (with DropTargetDropEvent's getTransferable() method). From there, you can query if the Transferable instance supports the DataFlavor you're looking for, which is a good thing to do because once your DropTarget is active, it can receive drops from anything else that can initiate the DnD process (another unaccounted-for component, for example). Once you've ascertained that the Transferable instance is the one you're looking for, call DropTargetDropEvent's acceptDrop() method, with the appropriate DnD action constant.

From there, things gets fuzzy. Ideally (and according to the documentation), you should be able to simply call the Transferable's getTransferData() method, cast the resulting Object to the type you're working with, and then do whatever with it (set an internal property, redo layout, rerender display, and so on). However, JavaSoft has thrown a middle layer in the mix -- proxies. If the DnD process is contained within the same instance of a JVM (for example, the DnD process started and ended in the same app), then there aren't any proxies. Otherwise, a TransferableProxy is applied to shield the data through the transfer. If the proxy has been applied, the resulting Object comes out as an InputStream. In Listing Two, the ColoredCanvas' drop() method handles both situations. As such, for the moment, only Strings can be transferred intact with or without the proxy. The proxy's presence usually adds garbage to the beginning of the String transferred, so I put in the preceding colon to match.

At the end of the drop() method, you clean up things by calling the DropTargetContext's dropComplete() method (obtained from the DropTargetDropEvent) with the Boolean argument matching whether or not the operation was a success.

At this writing and the beta 3 release of JDK 1.2, JFC's DnD support is at the initial phase of implementation. A large number of bugs remain, mostly dealing with the transferal of data. I'll be the first to admit that my code doesn't work on all platforms, especially with cases that are not intra-JVM. The upcoming beta 4 release of JDK 1.2 holds promise, however, with bug fixes. It will also introduce file DnD support, so you can drag an icon, representing a file from your operating systems' filesystem, and have your component be able to handle it appropriately. For example, a cool approach would be a text component that could handle text files and once dropped, rendered the text contents of the file in its display. No matter the bug fixes, however, JFC will always remain the more complicated DnD paradigm.

Enter WFC

Microsoft's WFC offers an easier way to implement DnD in your Java project. Keep in mind, however, that WFC takes advantage of existing Windows technologies, and as such, cannot be guaranteed to run cross-platform. Still, its architecture is similar to JFC. Your DnD implementation is broken up into instigators and drop handlers.

To initiate the DnD process, you again decide what the stimulus will be and upon its occurrence, you simply create an ObjectBag, stuff data inside, and then call WDragSession's beginDrag() method, with the instance of the ObjectBag and the type of DnD process. A sample WFC DnD initiator also is available electronically.

ObjectBag is interesting because it contains what's transferred along the whole process. Each content of ObjectBag has three representations: a Category, its class, and an instance of itself, all of which can be queried by the ObjectBag's helper methods. It's also interesting to note that with this paradigm, you don't have to worry about proxies and whether or not the DnD process is contained within the same JVM instance.

As with JFC's implementation, you do need to decide what type of action the DnD process will undertake (copying, moving, and so on). Again, most of the time, this action can be determined from the trigger event with its modifiers. If the object's going to be copied or linked, then simply call the beginDrag() method and you're done. Otherwise (the object is going to be moved), you'll need to implement a TransferSession to handle the DnD session and provide an implementation of the deleteSource() method so the original object is deleted, giving the overall impression that the object has been moved.

Handling drops in WFC involves implementing the DragHandler interface. Again, just as in JFC, your DnD initiator and handler can be the same class. One note: WFC DragHandlers must be heavyweight components, which require a peer. Extensions of Component or Container will not work. ColoredCanvas.java (available electronically) is code for handling drops in WFC. Through the DragSession's getTransferData() method, you receive a MetaObject, from which you can get the Object being transferred with MetaObject's getObject() method. The ColoredCanvas class (available electronically) does define the dragOver() method, checking to see if the Object that's inside the DnD process matches what you're looking for. This provides more interaction with users and also protects the drop() method from handling unexpected data.

WFC's DnD support is overwhelmingly simple. Since WFC takes advantage of the Win32 APIs through J/Direct and the Microsoft JVM, it can make assumptions and shortcuts that JFC cannot.

Conclusion

Drag-and-drop support brings Java to the next generation, providing users with the intuitive interface that is beyond demand, but simply expected in today's applications. While neither the JFC or WFC DnD implementations are ready for full development, now is the best time to start learning so when they are ready, you'll be able to fulfill user expectations.

DDJ

Listing One

//DragSrc.javaimport java.awt.*;
import java.awt.event.*;
import java.awt.dnd.*;
import java.awt.datatransfer.*;


</p>
public class DragSrc extends Component implements DragSourceListener {
    private Color m_clrColor = Color.red;
    private boolean m_bAddedYet = false;
    public DragSrc() {
        this(Color.red);
    }
    public DragSrc(Color c) {
        super();
        m_clrColor = c;
        enableEvents(AWTEvent.MOUSE_EVENT_MASK);
    }
    public void addNotify() {
        super.addNotify();
        m_bAddedYet = true;
    }
    public void paint(Graphics g) {
        if (m_bAddedYet) {
            Dimension d = getSize();
            g.setColor(m_clrColor);
            g.fill3DRect(0, 0, d.width, d.height, true);
        }
    }
    public void processMouseEvent(MouseEvent e) {
        if (e.getID() == MouseEvent.MOUSE_PRESSED) {
DragSource ds = DragSource.getDefaultDragSource();
            String str = ":" + m_clrColor.getRed() + ";";
            str += m_clrColor.getGreen() + ";";
            str += m_clrColor.getBlue();
            StringSelection t = new StringSelection(str);
            ds.startDrag(this, e, DnDConstants.ACTION_COPY, 
                       DragSource.DefaultCopyDrop, null, null, t, this);
        }
    }
    public void dragDropEnd(DragSourceDropEvent dsde) { ; }
    public void dragEnter(DragSourceDragEvent dsde) { ; }
    public void dragExit(DragSourceEvent dsde) { ; }
    public void dragOver(DragSourceDragEvent dsde) { ; }
    public void dropActionChanged(DragSourceDragEvent dsde) { ; }
}

Back to Article

Listing Two

/* ColoredCanvas.java * This handles drop events, specificially when
 * Colors are dropped in.
*/


</p>
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.io.*;


</p>
public class ColoredCanvas extends Component implements DropTargetListener {
    private boolean m_bAddedYet = false;
    private Color m_clrColor = Color.white;
    private DataFlavor ccDataFlavor = DataFlavor.stringFlavor;
 
    public ColoredCanvas() {
        this(Color.white);
    }
    public ColoredCanvas(Color c) {
        super();
        
        setDropTarget(new DropTarget(this, this));
        m_clrColor = c;
    }
    public void paint(Graphics g) {
        if (m_bAddedYet) {
            Dimension d = getSize();
            String str = "Color Me!";
            int x = d.width/2 - (g.getFontMetrics()).stringWidth(str)/2;
            int y = d.height/2 - (g.getFontMetrics()).getHeight();
            g.setColor(m_clrColor);
            g.fillRect(0, 0, d.width, d.height);
            g.setColor(Color.black);
            g.drawString(str, x, y);
        }
    }
    public void addNotify() {
        super.addNotify();
        m_bAddedYet = true;
    }
    public void dragEnter(DropTargetDragEvent dtde) {
        DataFlavor df[] = dtde.getCurrentDataFlavors();
        for (int i=0;i<df.length;i++) {
            if (df[i].equals(ccDataFlavor)) {
                dtde.acceptDrag(DnDConstants.ACTION_COPY);
                return;
            }
        }
        dtde.rejectDrag();
    }
    public void dragExit(DropTargetEvent dte) { ; }
    public void dragOver(DropTargetDragEvent dtde) { ; }


</p>
    private Color parseColor (String color) {
        color = color.substring(color.indexOf(":")+1);
        int r, g, b;
        int index1 = color.indexOf(";");
        r = Integer.parseInt(color.substring(0, index1));
        int index2 = color.indexOf(";", index1+1);
        g = Integer.parseInt(color.substring(index1+1, index2));
        b = Integer.parseInt(color.substring(index2+1));
        return new Color(r,g,b);
    }
    public void drop(DropTargetDropEvent dtde) {
        DropTargetContext dtc = dtde.getDropTargetContext();
        Transferable t = dtde.getTransferable();
        if (!t.isDataFlavorSupported(ccDataFlavor)) {
            dtc.dropComplete(false);
            return;
       }
        dtde.acceptDrop(DnDConstants.ACTION_COPY);
        Object obj;
        Color theNewColor = null;
        try {
            obj = t.getTransferData(ccDataFlavor);
        }
        catch (IOException ex) {
            dtc.dropComplete(false);
            return;
        }
        catch (UnsupportedFlavorException ex) {
            dtc.dropComplete(false);
            return;
        }
        if (obj!=null&&obj instanceof String) {
            theNewColor = parseColor((String)obj);
        }
        if (obj!=null&&obj instanceof InputStream) {
            InputStream input = (InputStream)obj;
            byte[] buffer = new byte[64];
            String color;
            try {
                int count = input.read(buffer);
                while (count!=-1) {
                    color = new String(buffer, 0, count);
                    theNewColor = parseColor(color);
                    count = input.read(buffer);
                }
            }
            catch (IOException e) {
                dtc.dropComplete(false);
                return;
            }
            try {
                input.close();
            }
            catch (IOException e) {
                dtc.dropComplete(false);
                return;
            }
        }
        if (theNewColor!=null) m_clrColor = theNewColor;
        repaint();
        dtc.dropComplete(true);
    }
    public void dropActionChanged(DropTargetDragEvent dtde) { ; }
}

Back to Article


Copyright © 1998, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video