论坛首页 Java企业应用论坛

[DnD]Swing Drag And Drop 分析笔记

浏览 5699 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2010-03-03   最后修改:2010-03-04
DnD(Drag And Drop)对于程序的用户友好度至关重要

常见的, 把一个文件直接拖入程序里, 把item从一个list拖到另一个list

最几天在学习Swing的DnD机制, 被搞得一塌糊涂, 晕头转向的忙活了好几个晚上, 终于理出一点头绪来:

总体来说, DnD 包括这几个类: DragSource, DragGestureRecognizer, DragGestureListener, DragSourceAdapter, DropTargetAdapter, TransferHandler, Transferable, DataFlavor

DragSoure: 环境变量, 生成DragGestureRecognizer
DragGestureRecognizer: 环境变量, 绑定DragGestureListener给指定的Jcomponent
DragGestureListener: 拖拽事件开始的源头
DragSourceAdapter: JComponent Source Listener: dragEnter, dragOver, dragExit, dragDropEnd
DropTargetAdapter: JComponent Target Listener: dropEnter,, drapOver, dragExit, drop

TransferHandler: drag & drop event handler, 从官方的源代码设计来看, DragSourceAdapter, DropTargetAdapter都包含在里面, 类似代理器的设计模式

Transferable: DnD事件的统一数据接口

DataFlavor:DnD事件的统一数据


先以简单的例子来演示DnD的机制

一 Drag
1.给 JTextField 添加DnD:
	JTextField txt1=new JTextField();
	txt1.setDragEnabled(true);

虽然只需以上2行代码即可, 但其实Swing在背后给我们做了很多事情
首先, jframe生成的时候, DataSourse和DragGestureRecognizer已经生成,
并给所有textfield, textarea之类的控件添加了DnD事件,
其中所有的jtextfield控件都绑定给
"javax.swing.plaf.basic.BasicTextUI$TextTransferHandler@4a63d8",
而这个类的内部也生成有DragSoure和DropTarget所有接口的实现

然后, 当你在jtextfield里选中所有文字, 再拖拽的时候, 就会激活DnD事件
具体顺序(Drag):
TransferHandler exportAsDrag->getSourceActions->createTransferabled->exportDone->dragGestureRecognized

多尝试几次, 你会发现, 其实不选中所有文件而直接拖拽, 也会激活DnD事件
TransferHandler dragGestureRecognized
更不可思议的是, 哪怕 txt1.setDragEnabled(false)也一样, 这是很有用的


2.给 Jlabel 添加DnD
官方给出的演示
/**
 * add label drag action with mousePressed
 * @author YS
 */
public class DragAndDropDemo2 {

	JFrame frame;
	Container container;
	JLabel lbl1;
	JLabel lbl2;
	JTextField txt1;
	JTextField txt2;
	JLabel lbl3;
	JTextArea txt3;

	public DragAndDropDemo2() {
		frame = new JFrame("DRAG AND DROP DEMO");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setLayout(null);
		container = frame.getContentPane();
		frame.setBounds(100, 100, 400, 300);
		addComponent();
		frame.setVisible(true);
	}

	private void addComponent() {
		lbl1 = new JLabel("text1");
		lbl2 = new JLabel("text2");
		txt1 = new JTextField();
		txt2 = new JTextField();
		lbl1.setBounds(10, 10, 40, 20);
		lbl2.setBounds(10, 40, 40, 20);
		txt1.setBounds(60, 10, 300, 20);
		txt2.setBounds(60, 40, 300, 20);
		txt1.setDragEnabled(true);
		txt2.setDragEnabled(true);

		container.add(lbl1);
		container.add(lbl2);
		container.add(txt1);
		container.add(txt2);

		addLabelDragEvent(lbl1);
	}

	private void addLabelDragEvent(JLabel lbl1) {
		lbl1.addMouseListener(new MouseAdapter() {

			@Override
			public void mousePressed(MouseEvent e) {
				JComponent com=(JComponent)e.getSource();
				TransferHandler hander=new TransferHandler("text");
				com.setTransferHandler(hander);
				hander.exportAsDrag(com, e, DnDConstants.ACTION_COPY);
			}
		});
	}

	public static void main(String[] args){
		SwingUtilities.invokeLater(new Runnable() {

			public void run() {
				new DragAndDropDemo2();
			}
		});
	}
}

由于label不能设计setDragEnabled(true), 所以需要人为的激活DnD事件, 一开始时, jframe并没有为jlabel绑定transferhanlder, 所以必须自己创建, 在new TransferHandler("text")的同时, drag & drop事件也已经生成了, 带"text"参数, 是创建能接受文本内容的handler, 具体的swing已经为我们做好了, 能用预设的最好都用预设的, 然后在mousePressed时激活exportAsDrag, 然后再转到 handler内部的dragGestureRecognized接口实现, swing已经帮我们作好获取label里面文本并生成transferable的工作了, 最终调用内部的startDrag开始拖拽

自己写另外一种实现
/**
 * add label drag action with dragGestureRecognizedListener
 * @author YS
 */
public class DragAndDropDemo3 {

	JFrame frame;
	Container container;
	JLabel lbl1;
	JLabel lbl2;
	JTextField txt1;
	JTextField txt2;
	JLabel lbl3;
	JTextArea txt3;

	public DragAndDropDemo3() {
		frame = new JFrame("DRAG AND DROP DEMO");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setLayout(null);
		container = frame.getContentPane();
		frame.setBounds(100, 100, 400, 300);
		addComponent();
		frame.setVisible(true);
	}

	private void addComponent() {
		lbl1 = new JLabel("text1");
		lbl2 = new JLabel("text2");
		txt1 = new JTextField();
		txt2 = new JTextField();
		lbl1.setBounds(10, 10, 40, 20);
		lbl2.setBounds(10, 40, 40, 20);
		txt1.setBounds(60, 10, 300, 20);
		txt2.setBounds(60, 40, 300, 20);
		txt1.setDragEnabled(true);
		txt2.setDragEnabled(true);

		container.add(lbl1);
		container.add(lbl2);
		container.add(txt1);
		container.add(txt2);

		addLabelDragEvent(lbl1);
	}

	private void addLabelDragEvent(JLabel lbl1) {
		DragSource ds=DragSource.getDefaultDragSource();
		ds.createDefaultDragGestureRecognizer(lbl1, DnDConstants.ACTION_COPY, new DragGestureAdapter());
	}

	public static void main(String[] args){
		SwingUtilities.invokeLater(new Runnable() {

			public void run() {
				new DragAndDropDemo3();
			}
		});
	}

	class DragGestureAdapter implements DragGestureListener{

		public void dragGestureRecognized(DragGestureEvent e) {
			System.out.println("Drag dragGestureRecognized");
			JComponent c = (JComponent) e.getComponent();
			Transferable t=createTransferable(c);
			e.startDrag(DragSource.DefaultCopyNoDrop, t, new DragAdapter());
		}
	}

	class DragAdapter extends DragSourceAdapter{

		@Override
		public void dragEnter(DragSourceDragEvent dsde) {
			Component com=(Component)dsde.getDragSourceContext().getComponent();
			Set<Class> set=new LinkedHashSet<Class>();
			set.add(JLabel.class);
			set.add(JTextField.class);
			if(set.contains(com.getClass())){
				dsde.getDragSourceContext().setCursor(DragSource.DefaultCopyDrop);
			}
		}

		@Override
		public void dragExit(DragSourceEvent dse) {
			Component com=(Component)dse.getDragSourceContext().getComponent();
			Set<Class> set=new LinkedHashSet<Class>();
			set.add(JLabel.class);
			set.add(JTextField.class);
			if(set.contains(com.getClass())){
				dse.getDragSourceContext().setCursor(DragSource.DefaultCopyNoDrop);
			}
		}

	}

    protected Transferable createTransferable(JComponent c) {
        System.out.println("DragGestureAdapter createTransferable");
        //return super.createTransferable(c);
        String text="";
        Component com=c;
        String type=com.getClass().getSimpleName();
        if("JLabel".equals(type)){
            text=type+((JLabel)com).getText();
        }else if("JTextField".equals(type)){
            text=type+": "+((JTextField)com).getText();
        }else if("JTextArea".equals(type)){
            text=type+": "+((JTextArea)com).getText();
        }
        Transferable t=new StringSelection(text);
        return t;
    }
}

这样也可以实现同样的效果, 原因在于: 其实mouseDragged的时候, 也会判断dragGestureRecognized, 因为这里给label绑定了dragGestureRecognized接口的实现, 所以DnD机制还是开始了, 从startDrag开始
这里要说一下鼠标的手势, 一开始应当指定的是DragSource.DefaultCopyNoDrop, 会是一个不可复制的鼠标, 如果指定DragSource.DefaultCopyDrop的话, 就是一个带+号的复制鼠标, 这样会误人视听, 鼠标在进入别的控件时, 会激发dragEnter事件, 在离开控件时, 激活dragExit, 要注意鼠标的手势

对比以上3个例子, 有几个比较重要的地方,
jtextfield; setDragEnabled(true),全选并拖拽时激发transferhandler的exportDrag, 然后才是createTransferabled, dragDestrueGeconized, 最终startDrag, (startDrag是在dragDestrueGeconized里调用的)

label(官方): 通过mousePress, 人为调用exportDrag, 然后的过程跟jtextfield一样

label(自定义): 通过绑定dragDestrueGeconized给jlabel, 然后dragDestrueGeconized在鼠标拖拽时被激活, transferable必须在dragDestrueGeconized里生成

所以, 可以得到, dragDestrueGeconized接口总会在鼠标拖拽(非mousePress)时

-----------------------------------------------Separator-----------------------------------------------

二 Drop
Drop的触发顺序: 目标控件的DropTarget的 drop(松开鼠标左键)->获取目标控件的TransferHandler, 并调用它的canImport, 如果为true, 则执行acceptDrop ,然后调用importData, 最终dropComplete

仔细分析, 里面其实有2个地方容易混乱, 一个是droptarget的drop方法, 另一个是transferhandler的importData方法,
2个方法都可以给我们重载代码, 但要通过drop模似swing的路线走到importData是比较困难的, 而且importData方法是后面, 所以, 推荐优先尝试在importData里面实现我们要的东西

简单示例
1. 从JTextField拖曳内容给JLabel, TransferHandler实现
/**
 * add label drop action with transferhandler("text")
 * @author YS
 */
public class DragAndDropDemo4 {

	JFrame frame;
	Container container;
	JLabel lbl1;
	JLabel lbl2;
	JTextField txt1;
	JTextField txt2;
	JLabel lbl3;
	JTextArea txt3;

	public DragAndDropDemo4() {
		frame = new JFrame("DRAG AND DROP DEMO");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setLayout(null);
		container = frame.getContentPane();
		frame.setBounds(100, 100, 400, 300);
		addComponent();
		frame.setVisible(true);
	}

	private void addComponent() {
		lbl1 = new JLabel("text1");
		lbl2 = new JLabel("text2");
		txt1 = new JTextField();
		txt2 = new JTextField();
		lbl1.setBounds(10, 10, 40, 20);
		lbl2.setBounds(10, 40, 40, 20);
		txt1.setBounds(60, 10, 300, 20);
		txt2.setBounds(60, 40, 300, 20);
		txt1.setDragEnabled(true);
		txt2.setDragEnabled(true);

		container.add(lbl1);
		container.add(lbl2);
		container.add(txt1);
		container.add(txt2);

		addLabelDropEvent(lbl1);
	}

	private void addLabelDropEvent(JLabel lbl1) {
		lbl1.setTransferHandler(new TransferHandler("text"));
	}

	public static void main(String[] args){
		SwingUtilities.invokeLater(new Runnable() {

			public void run() {
				new DragAndDropDemo4();
			}
		});
	}
}

其实, 由于label一开始是没有transferhandler的, 所以只需要通过系统预设的transferhandler("text"), 与此同时生成了droptarget实例, 方便地实现了label的拖曳赋值
其系统机制: 当鼠标在label上released时, droptarget的drop方法被激活, 然后获取自身的transferhandler, 如果null, 则调用rejectDrop, 否则执行transferhandler.canImport判断这个控件是否能接受drop, 可以的话, 执行transferhandler.importData

2. 从JTextField拖曳内容给JLabel, DropTarget实现
/**
 * add label drop action with DropTarget
 * @author YS
 */
public class DragAndDropDemo5 {

	JFrame frame;
	Container container;
	JLabel lbl1;
	JLabel lbl2;
	JTextField txt1;
	JTextField txt2;
	JLabel lbl3;
	JTextArea txt3;

	public DragAndDropDemo5() {
		frame = new JFrame("DRAG AND DROP DEMO");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setLayout(null);
		container = frame.getContentPane();
		frame.setBounds(100, 100, 400, 300);
		addComponent();
		frame.setVisible(true);
	}

	private void addComponent() {
		lbl1 = new JLabel("text1");
		lbl2 = new JLabel("text2");
		txt1 = new JTextField();
		txt2 = new JTextField();
		lbl1.setBounds(10, 10, 40, 20);
		lbl2.setBounds(10, 40, 40, 20);
		txt1.setBounds(60, 10, 300, 20);
		txt2.setBounds(60, 40, 300, 20);
		txt1.setDragEnabled(true);
		txt2.setDragEnabled(true);

		container.add(lbl1);
		container.add(lbl2);
		container.add(txt1);
		container.add(txt2);

		addLabelDropEvent(lbl1);
	}

	private void addLabelDropEvent(JLabel lbl1) {
		lbl1.setDropTarget(new MyDropTargetAdapter());
	}

	class MyDropTargetAdapter extends DropTarget{

		@Override
		public void drop(DropTargetDropEvent e) {
			try {
				e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
				Component com=e.getDropTargetContext().getComponent();
				Transferable t=e.getTransferable();
				String text=(String)t.getTransferData(DataFlavor.stringFlavor);
				if(com instanceof JLabel){
					((JLabel)com).setText(text);
				}else if(com instanceof JTextField){
					((JTextField)com).setText(text);
				}
				e.dropComplete(true);
			} catch (Exception ex) {
				e.rejectDrop();
				ex.printStackTrace();
			}
		}
	}

	public static void main(String[] args){
		SwingUtilities.invokeLater(new Runnable() {

			public void run() {
				new DragAndDropDemo5();
			}
		});
	}
}

由于DropTarget的drop已经实现我想要的, 并且没有像swing那样全面的检查并调用transferhandler的canImport和importData, 所以, 整个drop动作就到此结束

注意e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE), 如果没有这个, 就表示没有接受drop, swing就不会给你transable和dataflavor,
但在transferhandler的importData里, 在importData之前, 就swing就已经帮我们判断好acceptDrop还是rejectDrop, 结束后还有dropComplete

通过上面2种方法都可以实现drop动作, 但出于代码的严整性和程序的整体观念考虑, 还是推荐第一种, 在transferhandler的importData里面实现

to be continue...
   发表时间:2010-03-08  
分析的很好啊,最近也在学习 拖拽的应用。
0 请登录后投票
   发表时间:2010-08-28  
拖拽确实比较头疼呀!~
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics