Java - 如何拖放 JPanel 及其组件

Posted

技术标签:

【中文标题】Java - 如何拖放 JPanel 及其组件【英文标题】:Java - How to drag and drop JPanel with its components 【发布时间】:2012-06-27 10:34:11 【问题描述】:

我有一个关于拖放的问题: 我可以删除标签、文本或图标。但我想拖放一个 JPanel 及其所有组件(标签、文本框等)。

我该怎么做?

【问题讨论】:

没问题。你能告诉我们你到目前为止得到了什么吗? 我按照这种方式exampledepot.com/egs/javax.swing/label_LblCp.html 将文本或图标拖放到 Jlabel 上。现在我找不到拖放 Jpanel 内容的方法。如果我有一个包含文本框和标签的 Jpanel 或此 Jpanel 包含的任何内容,如何拖动此 Jpanel 并将其放在同一表单上的另一个 Jpnael 上。提前致谢 另见Introduction to DnD “新”传输处理程序 API 的限制可能过于严格,尽管它确实让内置组件的生活变得更加轻松 【参考方案1】:

此解决方案有效。一些注意事项。

我没有使用 TransferHandler API。我不喜欢它,它太严格了,但这是个人的事情(它做什么,它做得很好),所以这可能不符合你的期望。

我正在使用 BorderLayout 进行测试。如果您想使用其他布局,您将不得不尝试弄清楚这一点。 DnD 子系统确实提供有关鼠标点的信息(移动和放下时)。

那么我们需要什么:

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

    


可转让。某种包装器,用一堆 DataFlavor(在我们的例子中,只是 PanelDataFlavor)包装数据(我们的 JPanel)

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;

    


一个“DragGestureListener”

为此,我创建了一个简单的 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 下是这样,这导致了许多问题 ?。

当调用DragSource#startDrag 时,第一个DragGestureHandler 导致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);

重要的是要注意,上述这些修改可能不是必需的,但在我让操作再次工作之后它们就存在了......

我遇到的另一个问题是一堆NotSerializableExceptions ?

我需要更新 DragGestureHandlerDropHandler 类...

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 由被拖动的同一组件管理可能会导致所有问题,但我没有时间调查

需要注意的是,我不会提示也不会容忍以这种方式操作组件,因为这样很容易导致解决方案可能在今天有效,但明天就无效的情况。我更喜欢转移状态或数据——更稳定。

我已经尝试了十几个基于原始答案中提出的相同概念的其他示例,它们只是转移状态并且它们都可以正常工作,只是在尝试转移 Components 时它失败了 - 直到上述修复申请了?

【讨论】:

非常感谢这个有用的详细答案 上面给出的代码几乎可以工作。见:java forum question about this answer 很公平。我的评论太短了。让我们调查一下。在DragGestureHandler::dragGestureRecognized 方法中,变量被称为parent,因此隐藏了实例的parent 变量。这很容易解决。当我使用给定的代码时,我得到一个java.awt.dnd.InvalidDnDOperationException: Cannot find top-level for the drag source component。如果我将第一行(直到 parent.repaint(); 放在方法的底部,则不再抛出异常。不同之处在于 new PanelTransferable(getPanel()); 从中获取面板。为什么会这样? @Albert 猜猜看,自从我写 API 后,API 发生了变化 @Albert 记住,这段代码是 4 年前写的,可能是用 Java 6 编写的【参考方案2】:

该代码对 MadProgrammer 有很大帮助。对于任何想要使用这些类,但想要从您正在拖动的面板中的按钮启动拖动的人,我只是将扩展的 JPanel 替换为一个 JButton,该 JButton 在构造函数中使用面板:

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

【讨论】:

以上是关于Java - 如何拖放 JPanel 及其组件的主要内容,如果未能解决你的问题,请参考以下文章

JPanel 中的 Java 自定义拖放

java中在如何设置JPanel等组件的大小

Java 自己的组件通过拖放进行可视化连接

java中如何缩小两个jpanel间的距离

无论其大小如何,如何在 JPanel 中居中一组组件

java/swing:两个子窗口的拖放问题