Java - 如何拖放JPanel及其组件

14
我有一个与拖放相关的问题: 我可以拖放标签、文本或图标。但是我想拖放一个包含所有组件(标签、文本框等)的JPanel。
我该如何做到这一点?

没问题。你能给我们展示一下你目前的进展吗? - Guillaume Polet
我按照这个链接 http://www.exampledepot.com/egs/javax.swing/label_LblCp.html 的方式实现了在JLabel上拖放文本或图标。但是现在我找不到在JPanel上拖放内容的方法。如果我有一个包含文本框和标签或其他组件的JPanel,如何将该JPanel拖动并放置到同一表单上的另一个JPanel中。 提前致谢。 - Mariam
请参阅DnD入门 - trashgod
“新的”传输处理程序API可能过于严格,尽管它确实使其内置组件的生活变得更加容易。 - MadProgrammer
2个回答

28

这个解决方案可行。但是有一些注意事项。

我没有使用TransferHandler API。我不喜欢它,因为它太过限制性,但这只是我的个人想法(它所做的事情做得很好),所以这可能不能满足您的期望。

我使用了BorderLayout进行测试。如果您想使用其他布局,您需要尝试并弄清楚如何实现。拖放子系统确实提供有关鼠标指针(在移动和放置时)的信息。

那么我们需要什么:

DataFlavor。我选择这样做是因为它允许更大程度的限制。

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

    }

}

一个可传输的。它是一种包装器,将数据(我们的JPanel)与一堆数据类型(在我们的情况下,只有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;

    }

}

“拖动手势监听器”

为此,我创建了一个简单的DragGestureHandler,它以“JPanel”作为要拖动的内容。这使得手势处理程序能够自我管理。

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

        }
    }
}

好的,那么这是基础知识。 现在我们需要将它们全部连接起来...
所以,在我想要拖动的面板中,我添加了:
    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();

    }

使用添加/删除通知的原因是为了保持系统的清洁。这有助于防止在不再需要时向我们的组件传递事件。它还提供了自动注册。您可能希望使用自己的“setDraggable”方法。
那是拖动侧,现在是投放侧。
首先,我们需要一个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);

    }

}

最后,我们需要将放置目标注册给感兴趣的方。在那些能够支持拖放操作的容器中,你需要添加...
DropTarget dropTarget;
DropHandler dropHandler;

.
.
.

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

个人而言,我在addNotify中初始化,在removeNotify中销毁

dropTarget.removeDropTargetListener(dropHandler);

关于addNotify的简短说明,我曾多次连续调用它,因此您可能需要仔细检查是否已经设置了拖放目标。

就这些。

您可能还会发现以下内容有趣:

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

即使只是出于兴趣,也不应该浪费时间去查看它们。

2018年更新

因此,在原始代码编写后的4年中,API的工作方式似乎发生了一些变化,至少在MacOS下会导致一些问题。

首先,DragGestureHandler在调用DragSource#startDrag时会导致NullPointerException。这似乎与将容器的parent引用设置为null(通过从父容器中删除它)有关。

因此,我修改了dragGestureRecognized方法,在调用DragSource#startDrag之后从父面板中删除panel...

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

我还修改了DragGestureHandler#dragDropEnd方法。

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

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

重要的是要注意,这些修改可能并不是必需的,但它们存在的原因是在我使操作再次正常工作后发现的...
我还遇到了一堆 NotSerializableException 的问题。
我需要更新 DragGestureHandler 和 DropHandler 类...
public class DragGestureHandler implements DragGestureListener, DragSourceListener, Serializable {
    //...
}

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

可运行的示例...

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

}
DragGestureHandlerDropHandlerPanelDataFlavorPanelTransferable类保持不变,除了我上面提到的更改。所有这些类都是独立的外部类,否则会导致额外的NotSerializableException问题。

注释

有可能让DragGestureHandler由正在拖拽的组件管理会导致整体问题,但我没有时间进行调查。

值得注意的是,我不提倡也不允许以这种方式操作组件,因为很容易陷入一种解决方案今天可以工作,明天就不能工作的情况。我更喜欢传输状态或数据-更加稳定。

我尝试了十几个基于原始答案中提出的相同概念的其他示例,这些示例仅传输状态并且没有问题,在尝试传输Component时失败,直到应用了上述修复方法。


2
非常感谢您提供这个详细而有帮助的答案。 - Mariam
上述代码几乎可以工作。 请参见:有关此答案的Java论坛问题 - Albert
好的。我的评论太短了。让我们来调查一下。在 DragGestureHandler::dragGestureRecognized 方法中,变量称为 parent,因此隐藏了实例的 parent 变量。这很容易解决。 当我使用给定的代码时,我会得到一个 java.awt.dnd.InvalidDnDOperationException: Cannot find top-level for the drag source component。如果我将前几行代码(直到parent.repaint();)放在方法的底部,那么异常就不再被抛出了。区别在于 new PanelTransferable(getPanel()); 从哪里获取面板。为什么会这样? - Albert
@Albert 我猜测,自从我写这个API以来,它已经发生了变化。 - MadProgrammer
@Albert 记住,这段代码是4年前写的,可能是在Java 6中编写的。 - MadProgrammer
@MadProgrammer 我可以确认,它完美地运行了。如果可能的话,我会再次点赞这个回答,因为在Stackoverflow上,还有谁像你一样关心自己的回答经过了四年仍然能用呢? - Albert

0
那段代码对于MadProgrammer来说是非常有帮助的。对于任何想要使用这些类,但又想从拖动的面板中的一个按钮开始拖动的人,我只需用一个JButton来代替扩展的JPanel,并在构造函数中将面板传入即可:
public class DragActionButton  extends JButton {
  private DragGestureRecognizer dgr;
  private DragGestureHandler dragGestureHandler;
  private JPanel actionPanel;
DragActionButton (JPanel actionPanel, String buttonText)
 {
 this.setText(buttonText);
 this.actionPanel = actionPanel;
 }   

@Override
public void addNotify() {

    super.addNotify();

    if (dgr == null) {

        dragGestureHandler = new DragGestureHandler(this.actionPanel);
        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();

}

}

当创建按钮时,您将执行以下操作:

  this.JButtonDragIt = new DragActionButton(this.JPanel_To_Drag, "button-text-here");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接