Java - How to Drag and Drop JPAnel with Its Components

Java - How to drag and drop JPanel with its components

This solution works. Some cavets to start with.

I didn't use the TransferHandler API. I don't like it, it's too restrictive, but that's a personal thing (what it does, it does well), so this might not meet your expectations.

I was testing with BorderLayout. If you want to use other layouts, you're going to have to try and figure that out. The DnD subsystem does provide information about the mouse point (when moving and dropping).

So what do we need:

A DataFlavor. I chose to do this because it allows a greater deal of restriction

public class PanelDataFlavor extends DataFlavor {

// This saves me having to make lots of copies of the same thing
public static final PanelDataFlavor SHARED_INSTANCE = new PanelDataFlavor();

public PanelDataFlavor() {

super(JPanel.class, null);

}

}

A Transferable. Some kind of wrapper that wraps the data (our JPanel) up with a bunch of DataFlavors (in our case, just the PanelDataFlavor)

public class PanelTransferable implements Transferable {

private DataFlavor[] flavors = new DataFlavor[]{PanelDataFlavor.SHARED_INSTANCE};
private JPanel panel;

public PanelTransferable(JPanel panel) {
this.panel = panel;
}

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

@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {

// Okay, for this example, this is overkill, but makes it easier
// to add new flavor support by subclassing
boolean supported = false;

for (DataFlavor mine : getTransferDataFlavors()) {

if (mine.equals(flavor)) {

supported = true;
break;

}

}

return supported;

}

public JPanel getPanel() {

return panel;

}

@Override
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {

Object data = null;
if (isDataFlavorSupported(flavor)) {

data = getPanel();

} else {

throw new UnsupportedFlavorException(flavor);

}

return data;

}

}

A "DragGestureListener"

For this, I created a simple DragGestureHandler that takes a "JPanel" as the content to be dragged. This allows the gesture handler to become self managed.

public class DragGestureHandler implements DragGestureListener, DragSourceListener {

private Container parent;
private JPanel child;

public DragGestureHandler(JPanel child) {

this.child = child;

}

public JPanel getPanel() {
return child;
}

public void setParent(Container parent) {
this.parent = parent;
}

public Container getParent() {
return parent;
}

@Override
public void dragGestureRecognized(DragGestureEvent dge) {

// When the drag begins, we need to grab a reference to the
// parent container so we can return it if the drop
// is rejected
Container parent = getPanel().getParent();

setParent(parent);

// Remove the panel from the parent. If we don't do this, it
// can cause serialization issues. We could overcome this
// by allowing the drop target to remove the component, but that's
// an argument for another day
parent.remove(getPanel());

// Update the display
parent.invalidate();
parent.repaint();

// Create our transferable wrapper
Transferable transferable = new PanelTransferable(getPanel());

// Start the "drag" process...
DragSource ds = dge.getDragSource();
ds.startDrag(dge, Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR), transferable, this);

}

@Override
public void dragEnter(DragSourceDragEvent dsde) {
}

@Override
public void dragOver(DragSourceDragEvent dsde) {
}

@Override
public void dropActionChanged(DragSourceDragEvent dsde) {
}

@Override
public void dragExit(DragSourceEvent dse) {
}

@Override
public void dragDropEnd(DragSourceDropEvent dsde) {

// If the drop was not successful, we need to
// return the component back to it's previous
// parent
if (!dsde.getDropSuccess()) {

getParent().add(getPanel());

getParent().invalidate();
getParent().repaint();

}
}
}

Okay, so that's basics. Now we need to wire it all together...

So, in the panel I want to drag, I added:

    private DragGestureRecognizer dgr;
private DragGestureHandler dragGestureHandler;

@Override
public void addNotify() {

super.addNotify();

if (dgr == null) {

dragGestureHandler = new DragGestureHandler(this);
dgr = DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(
this,
DnDConstants.ACTION_MOVE,
dragGestureHandler);

}

}

@Override
public void removeNotify() {

if (dgr != null) {

dgr.removeDragGestureListener(dragGestureHandler);
dragGestureHandler = null;

}

dgr = null;

super.removeNotify();

}

The reason for using the add/remove notify in this way is to keep the system clean. It helps prevent events from been delivered to our component when we no longer need them. It also provides automatic registration. You may wish to use your own "setDraggable" method.

That's the drag side, now for the drop side.

First, we need a DropTargetListener:

public class DropHandler implements DropTargetListener {

@Override
public void dragEnter(DropTargetDragEvent dtde) {

// Determine if we can actually process the contents coming in.
// You could try and inspect the transferable as well, but
// there is an issue on the MacOS under some circumstances
// where it does not actually bundle the data until you accept the
// drop.
if (dtde.isDataFlavorSupported(PanelDataFlavor.SHARED_INSTANCE)) {

dtde.acceptDrag(DnDConstants.ACTION_MOVE);

} else {

dtde.rejectDrag();

}

}

@Override
public void dragOver(DropTargetDragEvent dtde) {
}

@Override
public void dropActionChanged(DropTargetDragEvent dtde) {
}

@Override
public void dragExit(DropTargetEvent dte) {
}

@Override
public void drop(DropTargetDropEvent dtde) {

boolean success = false;

// Basically, we want to unwrap the present...
if (dtde.isDataFlavorSupported(PanelDataFlavor.SHARED_INSTANCE)) {

Transferable transferable = dtde.getTransferable();
try {

Object data = transferable.getTransferData(PanelDataFlavor.SHARED_INSTANCE);
if (data instanceof JPanel) {

JPanel panel = (JPanel) data;

DropTargetContext dtc = dtde.getDropTargetContext();
Component component = dtc.getComponent();

if (component instanceof JComponent) {

Container parent = panel.getParent();
if (parent != null) {

parent.remove(panel);

}

((JComponent)component).add(panel);

success = true;
dtde.acceptDrop(DnDConstants.ACTION_MOVE);

invalidate();
repaint();

} else {

success = false;
dtde.rejectDrop();

}

} else {

success = false;
dtde.rejectDrop();

}

} catch (Exception exp) {

success = false;
dtde.rejectDrop();
exp.printStackTrace();

}

} else {

success = false;
dtde.rejectDrop();

}

dtde.dropComplete(success);

}

}

Finally, we need to register the drop target with interested parties... In those containers capable of supporting the drop, you want to add

DropTarget dropTarget;
DropHandler dropHandler;

.
.
.

dropHandler = new DropHandler();
dropTarget = new DropTarget(pnlOne, DnDConstants.ACTION_MOVE, dropHandler, true);

Personally, I initialise in the addNotify and dispose in the removeNotify

dropTarget.removeDropTargetListener(dropHandler);

Just a quick note on addNotify, I have had this been called a number of times in succession, so you may want to double-check that you haven't already set up the drop targets.

That's it.

You may also find some of the following of interest

http://rabbit-hole.blogspot.com.au/2006/05/my-drag-image-is-better-than-yours.html

http://rabbit-hole.blogspot.com.au/2006/08/drop-target-navigation-or-you-drag.html

http://rabbit-hole.blogspot.com.au/2006/04/smooth-jlist-drop-target-animation.html

It would be waste not to check them, even if just out of interest.

2018 Update

So, after 4 years since the original code was written, there seems to have been some changes into how the API works, at least under MacOS, which are causing a number of issues .

First DragGestureHandler was causing a NullPointerException when DragSource#startDrag was been called. This seems to be related to setting the container's parent reference to null (by removing it from the parent container).

So, instead, I modified the dragGestureRecognized method to remove the panel from the parent AFTER DragSource#startDrag was called...

@Override
public void dragGestureRecognized(DragGestureEvent dge) {
// When the drag begins, we need to grab a reference to the
// parent container so we can return it if the drop
// is rejected
Container parent = getPanel().getParent();
System.out.println("parent = " + parent.hashCode());
setParent(parent);

// Remove the panel from the parent. If we don't do this, it
// can cause serialization issues. We could overcome this
// by allowing the drop target to remove the component, but that's
// an argument for another day
// This is causing a NullPointerException on MacOS 10.13.3/Java 8
// parent.remove(getPanel());
// // Update the display
// parent.invalidate();
// parent.repaint();

// Create our transferable wrapper
System.out.println("Drag " + getPanel().hashCode());
Transferable transferable = new PanelTransferable(getPanel());
// Start the "drag" process...
DragSource ds = dge.getDragSource();
ds.startDrag(dge, null, transferable, this);

parent.remove(getPanel());
// Update the display
parent.invalidate();
parent.repaint();
}

I also modified the DragGestureHandler#dragDropEnd method

@Override
public void dragDropEnd(DragSourceDropEvent dsde) {
// If the drop was not successful, we need to
// return the component back to it's previous
// parent
if (!dsde.getDropSuccess()) {
getParent().add(getPanel());
} else {
getPanel().remove(getPanel());
}
getParent().invalidate();
getParent().repaint();
}

And DropHandler#drop

@Override
public void drop(DropTargetDropEvent dtde) {
boolean success = false;
// Basically, we want to unwrap the present...
if (dtde.isDataFlavorSupported(PanelDataFlavor.SHARED_INSTANCE)) {
Transferable transferable = dtde.getTransferable();
try {
Object data = transferable.getTransferData(PanelDataFlavor.SHARED_INSTANCE);
if (data instanceof JPanel) {
JPanel panel = (JPanel) data;
DropTargetContext dtc = dtde.getDropTargetContext();
Component component = dtc.getComponent();
if (component instanceof JComponent) {
Container parent = panel.getParent();
if (parent != null) {
parent.remove(panel);
parent.revalidate();
parent.repaint();
}
((JComponent) component).add(panel);
success = true;
dtde.acceptDrop(DnDConstants.ACTION_MOVE);
((JComponent) component).invalidate();
((JComponent) component).repaint();
} else {
success = false;
dtde.rejectDrop();
}
} else {
success = false;
dtde.rejectDrop();
}
} catch (Exception exp) {
success = false;
dtde.rejectDrop();
exp.printStackTrace();
}
} else {
success = false;
dtde.rejectDrop();
}
dtde.dropComplete(success);
}

It's important to note that these above modifications probably aren't required, but they existed after the point I got the operations to work again...

Another issue I came across was a bunch of NotSerializableExceptions /p>

I was required to update the DragGestureHandler and DropHandler classes...

public class DragGestureHandler implements DragGestureListener, DragSourceListener, Serializable {
//...
}

public public class DropHandler implements DropTargetListener, Serializable {
//...
}

Runnable example...

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.io.Serializable;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test implements Serializable {

public static void main(String[] args) {
new Test();;
}

public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}

JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}

public class TestPane extends JPanel {

public TestPane() {
setLayout(new GridLayout(1, 2));

JPanel container = new OutterPane();

DragPane drag = new DragPane();
container.add(drag);

add(container);
add(new DropPane());
}

@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}

}

public class OutterPane extends JPanel {

public OutterPane() {
setBackground(Color.GREEN);
}

@Override
public Dimension getPreferredSize() {
return new Dimension(100, 100);
}

}

}

DragPane

import java.awt.Color;
import java.awt.Dimension;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import javax.swing.JPanel;

public class DragPane extends JPanel {

private DragGestureRecognizer dgr;
private DragGestureHandler dragGestureHandler;

public DragPane() {
System.out.println("DragPane = " + this.hashCode());
setBackground(Color.RED);
dragGestureHandler = new DragGestureHandler(this);
dgr = DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_MOVE, dragGestureHandler);
}

@Override
public Dimension getPreferredSize() {
return new Dimension(50, 50);
}

}

DropPane

import java.awt.Color;
import java.awt.Dimension;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import javax.swing.JPanel;

public class DropPane extends JPanel {

DropTarget dropTarget;
DropHandler dropHandler;

public DropPane() {
setBackground(Color.BLUE);
}

@Override
public Dimension getPreferredSize() {
return new Dimension(100, 100);
}

@Override
public void addNotify() {
super.addNotify(); //To change body of generated methods, choose Tools | Templates.
dropHandler = new DropHandler();
dropTarget = new DropTarget(this, DnDConstants.ACTION_MOVE, dropHandler, true);
}

@Override
public void removeNotify() {
super.removeNotify(); //To change body of generated methods, choose Tools | Templates.
dropTarget.removeDropTargetListener(dropHandler);
}

}

The DragGestureHandler, DropHandler, PanelDataFlavor and PanelTransferable classes remain the same, except for the changes I've mentioned above. All these classes are standalone, external classes, otherwise it causes additional NotSerializableException problems

Notes

It's possible that having the DragGestureHandler managed by the same component which is been dragged could be causing the over all issues, but I don't have the time to investigate

It should be noted that, I don't prompt nor condone manipulating components in this way, as it's way to easy to end up in situations where a solution might work today, but won't work tomorrow. I prefer to transfer state or data instead - much more stable.

I had tried a dozen other examples based around the same concept presented in the original answer which simply transferred state and they all worked without issue, it was only when trying to transfer Components it failed - until the above fix was applied /p>

How to drag and drop a JLabel into a JPanel

After looking at a few examples posted by @MadProgrammer I came up with a solution that extends both the JPanel and JLabel. Here is the JLabel class:

public class LayerItem extends JLabel {

public LayerItem(String text) {

this.setText(text);

this.setTransferHandler(new ValueExportTransferHandler(text));

this.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
JLabel lbl = (JLabel) e.getSource();
TransferHandler handle = lbl.getTransferHandler();
handle.exportAsDrag(lbl, e, TransferHandler.COPY);
}
});

}

protected static class ValueExportTransferHandler extends TransferHandler {

public static final DataFlavor SUPPORTED_DATE_FLAVOR = DataFlavor.stringFlavor;
private String value;

public ValueExportTransferHandler(String value) {
this.value = value;
}

public String getValue() {
return value;
}

@Override
public int getSourceActions(JComponent c) {
return DnDConstants.ACTION_COPY_OR_MOVE;
}

@Override
protected Transferable createTransferable(JComponent c) {
Transferable t = new StringSelection(getValue());
return t;
}

@Override
protected void exportDone(JComponent source, Transferable data, int action) {
super.exportDone(source, data, action);
// Clean up and remove the LayerItem that was moved
((LayerItem) source).setVisible(false);
((LayerItem) source).getParent().remove((LayerItem) source);
}

}
}

Here is the class for the JPanel:

public class LayerContainer extends JPanel {

public LayerContainer() {
this.setTransferHandler(new ValueImportTransferHandler());
this.setLayout(new GridBagLayout()); // Optional layout
this.setBorder(new CompoundBorder(new LineBorder(Color.DARK_GRAY), new EmptyBorder(20, 20, 20, 20))); // Optional border
}

protected static class ValueImportTransferHandler extends TransferHandler {

public static final DataFlavor SUPPORTED_DATE_FLAVOR = DataFlavor.stringFlavor;

public ValueImportTransferHandler() {
}

@Override
public boolean canImport(TransferHandler.TransferSupport support) {
return support.isDataFlavorSupported(SUPPORTED_DATE_FLAVOR);
}

@Override
public boolean importData(TransferHandler.TransferSupport support) {
boolean accept = false;
if (canImport(support)) {
try {
Transferable t = support.getTransferable();
Object value = t.getTransferData(SUPPORTED_DATE_FLAVOR);
if (value instanceof String) { // Ensure no errors
// TODO: here you can create your own handler
// ie: ((LayerContainer) component).getHandler()....
Component component = support.getComponent();
LayerItem j = new LayerItem((String) value);
((LayerContainer) component).add(j); // Add a new drag JLabel
accept = true;
}
} catch (Exception exp) {
exp.printStackTrace();
}
}
return accept;
}
}

}

Here is an example of how you could use this (drag from one JPanel to another and back again):

    JPanel left_panel = new LayerContainer();
panel_1.setBounds(28, 47, 129, 97);
add(panel_1);

LayerContainer right_panel = new LayerContainer();
layerContainer.setBounds(203, 47, 129, 97);
add(layerContainer);

JLabel lblToDrag = new LayerItem("Drag Me");
GridBagConstraints gbc_lblToDrag = new GridBagConstraints();
gbc_lblDragMe.gridx = 0;
gbc_lblDragMe.gridy = 0;
panel_right.add(lblToDrag, gbc_lblToDrag);

For future use, I'll create a onTransfer() method and create my own LayerContainerHandler() which overrites a run() method so each time a Label is moved to different Containers, it execute seperate actions.

Drag and Drop between two panels using swing?

This is not how drag and drop is accomplished in Java/Swing

There are plenty of examples on SO...

  • Drag and Drop custom object from JList into JLabel
  • Java - How to drag and drop JPanel with its components
  • how to drag and drop files from a directory in java
  • Java - visually drag a swing element

I'd also recommend that you checkout How to drag and drop with Java 2

Implementing drag-and-drop from one JPanel to another

May be this can help you.

Drag and Drop of complex custom objects in Java

Drag component within a JPanel using Gridbag layout manager

if the JComponent is the only component in the JPanel, the task is not that complicated. Here is a small demo program that does it (with a bonus of re-sizing the component in response to mouse wheel events) :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.LineBorder;

public class GridbagDragDemo extends JPanel {

/**
* Pixel width and height of positioned component.
*/
private float width, height;

/**
* Layout manger for this.
*/
private GridBagLayout gbl;

/**
* Layout horizontal weights for left and right gap, and component.
*/
private float leftWeight,xCompWeight, rightWeight;

/**
* Layout vertical weights for top and right gap, and component.
*/
private float topWeight,yCompWeight, bottomWeight;

/**
* Min and max weight values.
* These values can be changed to change the sensitivity of dragging.
* For better responsiveness W_MAX can be changed in respect to the JPanl's size.
* (also a different W_MAX can be set to horizontal and vertical axis.
*/
private float W_MIN = 0, W_MAX = 2;

/**
* Keep sum an even number for calculating (int) W_SUM/2
*/
private float W_SUM = W_MIN + W_MAX;

/**
* Represents the change in ratio between left / right weights
* and top/bottom weights for every pixel of mouse drag.
* The higher the factor the faster / more sensitive the
* component move is.
* Try different values to get the responsiveness you need.
*/
private float WEIGHT_DELTA = 0.01f;

/**
* Represents the change (in pixels) in component width and height
* and top/bottom weights for every mouse wheel notch.
* The higher the factor the faster / more sensitive the
* component resize.
* Try different values to get the responsiveness you need.
*/
private static final int ZOOM_FACTOR = 4;

/**
* Store mouse pressed position.
*/
private float pX, pY;

/**
* The dragged component
*/
private JComponent component;

public GridbagDragDemo() {

//set initial position to center
leftWeight = W_SUM/2 ; xCompWeight = 0; rightWeight = W_SUM/2;
topWeight = W_SUM/2 ; yCompWeight = 0; bottomWeight = W_SUM/2;

setPreferredSize(new Dimension(400, 300));

gbl = new GridBagLayout();
gbl.columnWidths = new int[] {0, 0, 0};
gbl.rowHeights = new int[] {0, 0, 0};
gbl.columnWeights = new double[]{leftWeight , xCompWeight, rightWeight };
gbl.rowWeights = new double[]{topWeight,yCompWeight, bottomWeight};
setLayout(gbl);
setBackground(Color.YELLOW);

component = new JPanel();
component.setPreferredSize(new Dimension(75,75));
component.setMinimumSize(new Dimension(15,15));
component.setMaximumSize(new Dimension(225,225));
component.setBackground(Color.ORANGE);
component.setBorder(new LineBorder(Color.black, 3));

//add drag listeners
component.addMouseMotionListener(new MouseMotionAdapter(){
@Override
public void mouseDragged(MouseEvent me) {

int mouseX = me.getXOnScreen();
int mouseY = me.getYOnScreen();

float moveX = mouseX - pX;
float moveY = mouseY - pY;

pX = mouseX;
pY = mouseY;

moveComp(moveX , moveY);

}
});
component.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent me) {

//store pressed position
pX = me.getXOnScreen();
pY = me.getYOnScreen();
}
});

//add resize listener
component.addMouseWheelListener(new MouseWheelListener() {

@Override
public void mouseWheelMoved(MouseWheelEvent me) {

//change sign so rolling "up" will be positive
reSizeComp(- me.getWheelRotation());
}
});

GridBagConstraints gbc_panel = new GridBagConstraints();
gbc_panel.fill = GridBagConstraints.BOTH;
gbc_panel.gridx = 1;
gbc_panel.gridy = 1;
add(component, gbc_panel);

width = component.getPreferredSize().width;
height = component.getPreferredSize().height;
}

private void moveComp(float moveX, float moveY) {

if(Math.abs(moveX)>0) {

leftWeight += WEIGHT_DELTA * moveX;
leftWeight = (float) setValueInRange(leftWeight, W_MIN, W_MAX);
rightWeight = W_SUM - leftWeight;
}

if(Math.abs(moveY)>0) {

topWeight += WEIGHT_DELTA * moveY;
topWeight = (float) setValueInRange(topWeight, W_MIN, W_MAX );
bottomWeight = W_SUM - topWeight;
}

gbl.columnWeights = new double[]{leftWeight,xCompWeight, rightWeight};
gbl.rowWeights = new double[]{topWeight, yCompWeight, bottomWeight};

refresh();
}

/**
*
*/
private void refresh() {

revalidate();
getParent().repaint();
}

private void reSizeComp(int notches) {

width += notches*ZOOM_FACTOR ; height += notches *ZOOM_FACTOR ;

//respect min / max component size
width = (float) setValueInRange(width, component.getMinimumSize().getWidth(),
component.getMaximumSize().getWidth() );
height = (float) setValueInRange(height, component.getMinimumSize().getHeight(),
component.getMaximumSize().getHeight() );
component.setPreferredSize(new Dimension((int)width,(int)height));

refresh();
}


Related Topics



Leave a reply



Submit