JLayeredPanel layout manager free moving objects

前端 未结 1 670
挽巷
挽巷 2021-01-25 07:09

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

相关标签:
1条回答
  • 2021-01-25 07:59

    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.

    Drag/Piece

    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();
                }
            }
        }
    
    }
    

    Drop/Cell/Grid

    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);
            }
        }
    
    }
    

    The Glue

    In Drag'n'Drop, there are two special classes which glue the drag to the drop....

    The 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);
        }
    
    }
    

    The 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 DataFlavors, 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;
        }
    
    }
    

    Putting it all together

    Because the components are self contained, putting it together is actually really easy...they basically take care of themselves...

    What a drag

    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);
        }
    
    }
    

    What's the catch?

    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

    0 讨论(0)
提交回复
热议问题