I have a game board, with 8 pieces that I would like to be able to move them anywhere on the jpanel. Currently, I can only do flow or grid layout, however this does not yield t
Drag'n'Drop is some serious work. When done right it can be really awesome, but be prepared for some serious heavy lifting and design work...
One approach is to try an generate self contained units of work, that is the piece is responsible for managing it's own drag and the cell/grid is responsible for managing the drop.
A Piece
is a movable game piece, which can be dragged to a new location.
The piece itself is responsible for managing the DragGestureRecognizer
which is used to initialise the drag process...
Because I wanted to display an icon in the piece, I choose to override JLabel
, as it provides the core functionality for this...
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.image.BufferedImage;
import javax.swing.JLabel;
public class PieceLabel extends JLabel {
private DragGestureHandler dragGestureHandler;
private DragGestureRecognizer dgr;
public PieceLabel() {
setHorizontalAlignment(CENTER);
setVerticalAlignment(CENTER);
}
@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();
}
public static class DragGestureHandler implements DragGestureListener, DragSourceListener {
private PieceLabel piece;
private Container parent;
public DragGestureHandler(PieceLabel child) {
this.piece = child;
}
public PieceLabel getPiece() {
return piece;
}
protected void setParent(Container parent) {
this.parent = parent;
}
protected 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 = getPiece().getParent();
setParent(parent);
// Remove the panel from the parent. If we don't do this, it
// can cause serialization issues. We could over come this
// by allowing the drop target to remove the component, but that's
// an argument for another day
parent.remove(getPiece());
// Update the display
parent.invalidate();
parent.repaint();
// Create our transferable wrapper
Transferable transferable = new PieceTransferable(getPiece());
// Start the "drag" process...
DragSource ds = dge.getDragSource();
// ds.startDrag(dge, Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR), transferable, this);
BufferedImage image = BoardDrag.createBufferedImage(piece.getIcon(), piece);
Point pp = piece.getLocation();
Point dp = dge.getDragOrigin();
int x = image.getWidth() / 2;
int y = image.getHeight() / 2;
ds.startDrag(dge, Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR), image, new Point(x, y), 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 sucessful, we need to
// return the component back to it's previous
// parent
if (!dsde.getDropSuccess()) {
getParent().add(getPiece());
getParent().invalidate();
getParent().repaint();
}
}
}
}
A cell/grid is just that, it makes up a single element within the over all grid/board. It can contain a Piece
(and in fact, you could easily configure it do reject everything else)
This manages the DropTarget
, which is responsible for detecting when something is dropped onto it...
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetContext;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import javax.swing.JComponent;
import javax.swing.JPanel;
public class Cell extends JPanel {
private DropTarget dropTarget;
private DropHandler dropHandler;
public Cell() {
setLayout(new BorderLayout());
}
@Override
public void addNotify() {
super.addNotify();
if (dropHandler == null) {
dropHandler = new DropHandler();
}
if (dropTarget == null) {
dropTarget = new DropTarget(this, DnDConstants.ACTION_MOVE, dropHandler, true);
}
}
@Override
public void removeNotify() {
if (dropTarget != null) {
dropTarget.removeDropTargetListener(dropHandler);
}
dropTarget = null;
dropHandler = null;
super.removeNotify();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(48, 48);
}
public class DropHandler implements DropTargetListener {
@Override
public void dragEnter(DropTargetDragEvent dtde) {
// Determine if can actual process the contents comming 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(PieceDataFlavor.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(PieceDataFlavor.SHARED_INSTANCE)) {
Transferable transferable = dtde.getTransferable();
try {
Object data = transferable.getTransferData(PieceDataFlavor.SHARED_INSTANCE);
if (data instanceof PieceLabel) {
PieceLabel piece = (PieceLabel) data;
DropTargetContext dtc = dtde.getDropTargetContext();
Component component = dtc.getComponent();
if (component instanceof JComponent) {
Container parent = piece.getParent();
if (parent != null) {
parent.remove(piece);
}
((JComponent) component).add(piece);
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);
}
}
}
In Drag'n'Drop, there are two special classes which glue the drag to the drop....
DataFlavor
The DataFlavor
is responsible for providing a means by which disconnected elements can determine not only what is been transferred, but how that data should be reconstituted...
For simplicity, I just PieceLabel.class
import java.awt.datatransfer.DataFlavor;
public class PieceDataFlavor extends DataFlavor {
public static final PieceDataFlavor SHARED_INSTANCE = new PieceDataFlavor();
public PieceDataFlavor() {
super(PieceLabel.class, null);
}
}
Transferable
The Transferable
is a wrapper class which allows data to be moved from one location to another, like the clipboard, for example.
This example is reasonably simple, but you could imagine that a Transferable
might contain multiple DataFlavor
s, depending on which DataFlavor
you want could change the type (or the manner in which you get) the data.
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
public class PieceTransferable implements Transferable {
private final DataFlavor[] flavors = new DataFlavor[]{PieceDataFlavor.SHARED_INSTANCE};
private final PieceLabel piece;
public PieceTransferable(PieceLabel piece) {
this.piece = piece;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return flavors;
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
// Okay, for this example, this is over kill, 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 PieceLabel getPanel() {
return piece;
}
@Override
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
Object data = null;
if (isDataFlavorSupported(flavor)) {
data = getPanel();
} else {
throw new UnsupportedFlavorException(flavor);
}
return data;
}
}
Because the components are self contained, putting it together is actually really easy...they basically take care of themselves...
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
public class BoardPane extends JPanel {
public BoardPane() {
setLayout(new GridLayout(8, 8));
int index = 0;
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
Cell cell = new Cell();
if (index % 2 == 0) {
cell.setBackground(Color.WHITE);
} else {
cell.setBackground(Color.BLACK);
}
add(cell);
index++;
}
index++;
}
try {
PieceLabel label = new PieceLabel();
BufferedImage image = ImageIO.read(getClass().getResource("/Piece01.png"));
label.setIcon(new ImageIcon(image));
setCellPiece(label, 0, 0);
} catch (IOException ex) {
Logger.getLogger(BoardDrag.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void setCellPiece(PieceLabel label, int row, int col) {
int index = (row * 8) + col;
Cell cell = (Cell) getComponent(index);
cell.removeAll();
cell.add(label);
}
}
That's right, you don't get everything of free.
You will have to implement the logic required to determine if a move is valid or not. The logic should be implemented in such away as to actively reject the drag. This might require you to add more information to the Transferable
so you can determine the start cell, for example.
I'd personally be looking to implement some kind of "rules" engine, which can be used by your DnD API so that it becomes pluggable