Java - 如何拖放 JPanel 及其组件
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/11201734/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java - How to drag and drop JPanel with its components
提问by Mariam
I have a question about dragging and droping: I can drop labels, text or icon. But I want to drag and drop a JPanel with all its components (Label, Textbox,..etc).
我有一个关于拖放的问题:我可以拖放标签、文本或图标。但我想拖放一个 JPanel 及其所有组件(标签、文本框等)。
How can I do this ?
我怎样才能做到这一点 ?
回答by MadProgrammer
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.
我没有使用 TransferHandler API。我不喜欢它,它限制太多,但这是个人的事情(它做什么,它做得很好),所以这可能不符合您的期望。
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).
我正在使用 BorderLayout 进行测试。如果你想使用其他布局,你将不得不尝试弄清楚。DnD 子系统确实提供有关鼠标点的信息(移动和放下时)。
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)
可转让。某种包装器,用一堆 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;
}
}
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.
为此,我创建了一个简单的 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();
}
}
}
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.
以这种方式使用添加/删除通知的原因是为了保持系统清洁。它有助于防止在我们不再需要事件时将事件传递给我们的组件。它还提供自动注册。您可能希望使用自己的“setDraggable”方法。
That's the drag side, now for the drop side.
这是拖拽的一面,现在是拖拽的一面。
First, we need a DropTargetListener:
首先,我们需要一个 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
就个人而言,我在 addNotify 中初始化并在 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.
只是关于 addNotify 的简短说明,我已经连续多次调用了它,因此您可能需要仔细检查您是否还没有设置放置目标。
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/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/08/drop-target-navigation-or-you-drag.html
http://rabbit-hole.blogspot.com.au/2006/04/smooth-jlist-drop-target-animation.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
2018年更新
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 .
因此,在编写原始代码 4 年后,API 的工作方式似乎发生了一些变化,至少在 MacOS 下,这导致了许多问题。
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).
首先DragGestureHandler
是导致一个NullPointerException
whenDragSource#startDrag
被调用。这似乎与将容器的parent
引用设置为null
(通过从父容器中删除它)有关。
So, instead, I modified the dragGestureRecognized
method to remove the panel
from the parent AFTER DragSource#startDrag
was called...
因此,相反,我修改了dragGestureRecognized
方法以在调用panel
后从父级中删除DragSource#startDrag
...
@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
我也修改了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();
}
And DropHandler#drop
和 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 NotSerializableException
s
我遇到的另一个问题是一堆NotSerializableException
s
I was required to update the DragGestureHandler
and DropHandler
classes...
我被要求更新DragGestureHandler
和DropHandler
类...
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
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
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
的DragGestureHandler
,DropHandler
,PanelDataFlavor
和PanelTransferable
类别保持不变,除了我上面提到的变化。所有这些类都是独立的外部类,否则会导致额外的NotSerializableException
问题
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
这有可能是具有DragGestureHandler
由被一直拖到可能会造成在所有问题上有着相同组件管理,但我没有调查时间
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 Component
s it failed - until the above fix was applied
我已经尝试了十几个基于原始答案中提出的相同概念的其他示例,这些示例只是转移状态,并且它们都可以正常工作,只有在尝试转移Component
时失败 - 直到应用上述修复
回答by pcalkins
That code is a HUGE help MadProgrammer. For anyone wanting to use those classes, but wants to initiate the drag from a button in the panel you are dragging, I simply replaced the extended JPanel with one for a JButton that takes the panel in the constructor:
该代码是一个巨大的帮助 MadProgrammer。对于任何想要使用这些类,但想要从您正在拖动的面板中的按钮开始拖动的人,我只是将扩展的 JPanel 替换为一个 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();
}
}
then you'd do this when creating the button:
那么你在创建按钮时会这样做:
this.JButtonDragIt = new DragActionButton(this.JPanel_To_Drag, "button-text-here");