下面的Demo主要用Java Swing实现了JTabbedPane内的tab可以拖拽到另一个JTabbedPane里的功能。
其实只要是用DnDTabbedPane生成的instance, 它里面所属的tab就可以随意拖拽到另一个DnDTabbedPane。
key words: DragGestureListener, DragSourceListener, Transferable, DropTargetListener.
/* * DnDTabbedPaneExe.java * Description: Initialize the UI and run the test. */ import java.awt.BorderLayout; import javax.swing.*; public class DnDTabbedPaneExe { public DnDTabbedPaneExe() { initUI(); } public void initUI() { JFrame test = new JFrame("Drag and Drop Tabs Test"); test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); test.setLocation(400, 400); test.setSize(1000, 450); // Our DnDTabbedPane here. DnDTabbedPane tabs = new DnDTabbedPane(); DnDTabbedPane tabs2 = new DnDTabbedPane(); JPanel panel = new JPanel(); panel.add(new JButton("1")); panel.add(new JButton("123")); ImageIcon icon = new ImageIcon("smallChrome.JPG"); tabs.addTab("Table",icon, initTable()); tabs.addTab("ButtonGroup",icon, panel); tabs.addTab("JTextArea",icon, new JTextArea("2")); tabs.addTab("JLabel",icon, new JLabel("3")); tabs.addTab("Button",icon, new JButton("4")); tabs.addTab("Table2",icon, initTable()); tabs2.addTab("Story", icon, new JTextArea("This is a story")); tabs2.addTab("A Button",icon, new JButton("A")); tabs2.addTab("B Button",icon, new JButton("B")); test.add(initTree(), BorderLayout.WEST); test.add(tabs, BorderLayout.EAST); test.add(tabs2, BorderLayout.CENTER); test.setVisible(true); } /* * Main */ public static void main(String[] args) { DnDTabbedPaneExe frame = new DnDTabbedPaneExe(); } // Methods initialize some tabs /* * Initialize JTree with default JTree example while new JTree() */ public class InitJTree extends JTree { public InitJTree() { super(); this.setAutoscrolls(true); } } /* * Return a JScrollPane with draggable JTree */ private JScrollPane initTree() { JTree myTree; myTree = new JTree(); myTree.setDragEnabled(true); myTree.setTransferHandler(new TreeTransferHandler());// -----TreeTransferHandler here------- JScrollPane treePane = new JScrollPane(myTree); return treePane; } /* * Return a JScrollPane with table accept the draggable Tree nodes */ private JScrollPane initTable() { JTable table = new JTable(8, 3); for (int i = 0; i < 8; i++) { table.setValueAt(i + ",0", i, 0); } table.setTransferHandler(new TableTransferHandler());// -----TableTransferHandler here------ return new JScrollPane(table); } }
/* * DnDTabbedPane.java * Description: Tabs of this DnDTabbedPane can be dragged and dropped. */ import java.awt.BorderLayout; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; import java.awt.TextArea; import java.awt.Window; import java.awt.datatransfer.*; import java.awt.dnd.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.ArrayList; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.plaf.basic.BasicTabbedPaneUI; import javax.swing.tree.TreePath; public class DnDTabbedPane extends JTabbedPane { private static ImageIcon _iconCloseRed = new ImageIcon("close-red.gif"); private static ImageIcon _iconCloseBlack = new ImageIcon("close-black.gif"); private static ImageIcon _iconIDE = new ImageIcon("IDE.JPG"); private static ImageIcon _iconChrome = new ImageIcon("chrome.PNG"); //private final static int BUTTON_HOT = 2; private final static int BUTTON_NORMAL = 1; private final static int BUTTON_NULL = 0; private int _preSelectedTab = -1; private int _preRolloverTab = -1; private boolean _bClosed = true; private int dragTabIndex = -1; /** * DnDTabbedPane class constructor: Here we initialize DragSourceListener * and DragGestureListnener */ public DnDTabbedPane() { super(); //setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); setAutoscrolls(true); initComponents(); // set JTabbedPane as DropTarget and DragSource here. new DragSource().createDefaultDragGestureRecognizer(DnDTabbedPane.this, DnDConstants.ACTION_COPY_OR_MOVE, new DnDDragGestureListener()); new DropTarget(DnDTabbedPane.this, DnDConstants.ACTION_COPY_OR_MOVE, new DnDTargetListener(), true); } /** * * Override AddTab with arguments: title and Component to the JTabbedPane, add close button to tab * @see javax.swing.JTabbedPane#addTab(java.lang.String, java.awt.Component) */ public void addTab(String title,Icon iconTab, Component comp) { add(comp); int index = indexOfComponent(comp); TabComponent tabComponent = new TabComponent(title, iconTab); setTabComponentAt(index, tabComponent); int indexTab = getSelectedIndex(); setTabCloseButtonStatus(indexTab, BUTTON_NORMAL); } /** * Control the appearance of close buttons on tabs. */ private void initComponents() { if (_bClosed) { // If current component needs to support the Close Button, we should add the // ChangeListener event to update the icon of Close Button according to the selected // status. addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { int indexTab = getSelectedIndex(); if (_preSelectedTab != indexTab) { // Remove the icon from previous selected tab setTabCloseButtonStatus(_preSelectedTab, BUTTON_NULL); // Set current selected tab to be BUTTON_NORMAL setTabNormalByMouse(indexTab); _preSelectedTab = indexTab; } else { //After moving, the original selected index in src is not changed, //but a new tab is selected, we have to enable close button for //this new tab. setTabNormalByMouse(_preSelectedTab); } } }); addMouseListener(new MouseAdapter() { public void mouseExited(MouseEvent e) { // If user move mouse from TabbedComponent to Close button, it will trigger the // mouseExited event, so we need to add the !mouseOnCloseButton(_preRolloverTab) // condition. otherwise icon will be sparkling obviously //if (_preRolloverTab >= 0 && _preRolloverTab != getSelectedIndex() && !mouseOnCloseButton(_preRolloverTab)) if (_preRolloverTab >= 0 && _preRolloverTab != getSelectedIndex()) { // If mouse exited TabbedPane and not on the close button and // current tab is not selected, it needs to clean up previous tab icon. setTabCloseButtonStatus(_preRolloverTab, BUTTON_NULL); } } }); // If tab is too much, it will be shown by multilayer. The width of header // will be bigger than that of tab component. At this time, if user move // mouse to the empty area we need to set the rollovered tab to be normal. addMouseMotionListener(new MouseAdapter() { public void mouseMoved(MouseEvent e) { int pointingTabIndex = tabForCoordinate(e.getPoint()); if (_preRolloverTab >= 0 && _preRolloverTab != pointingTabIndex ) { // If mouse enter another tab, we need to update current close button // and reset that of the leaving tab if (_preRolloverTab != getSelectedIndex()) setTabCloseButtonStatus(_preRolloverTab, BUTTON_NULL); //if _preRolloverTab == getSelectedIndex(), it will always be with a close button. /*else setTabCloseButtonStatus(_preRolloverTab, BUTTON_NORMAL);*/ } if (pointingTabIndex >= 0) { setTabNormalByMouse(pointingTabIndex); _preRolloverTab = pointingTabIndex; } } }); } } //Methods control behaviors of close button on tab. /** * Set the tab of this index to be normal with a black close button. * * _button.setRolloverIcon(New ImageIcon("close-red.jpg")); is used to control * the appearance when the mouse is over the close button. */ private void setTabNormalByMouse(int index) { setTabCloseButtonStatus(index, BUTTON_NORMAL); } /** * Reset the icon of close button on the indexed tab. */ private void setTabCloseButtonStatus(int indexTab, int buttonStatus) { if (indexTab >= 0 && indexTab < getTabCount()) { TabComponent tabComponent = (TabComponent) getTabComponentAt(indexTab); if (tabComponent != null) { tabComponent.setCloseButtonStatus(buttonStatus); } } } /** * According to the Point, get the related index of Tab. */ private int tabForCoordinate(Point p) { int index = getUI().tabForCoordinate(DnDTabbedPane.this, p.x, p.y); return index; } /** * Inner class DnDDragGestureListener implements DragGestureListener. * Start the drag here. */ class DnDDragGestureListener implements DragGestureListener { public void dragGestureRecognized(DragGestureEvent e) { //Get the drag source panel size int srcWidth = e.getComponent().getWidth(); int srcHeight = e.getComponent().getHeight(); System.out.println("Source size: " + srcWidth + "," + srcHeight); Point tabPt = e.getDragOrigin(); dragTabIndex = indexAtLocation(tabPt.x, tabPt.y); if (dragTabIndex < 0) { System.out.println("Please drag a tab"); return; } //GhostedDragImage image = new GhostedDragImage (e.getSource(),this,new Point(400,400),_iconIDE,new Point(5,5),true); try { System.out.println("<<Tab drag start...>>"); Cursor cursor = getToolkit().createCustomCursor(_iconChrome.getImage(), new Point(0,0), "usr"); //dge.startDrag(cursor, t, this); //e.startDrag(DragSource.DefaultMoveDrop,_iconIDE.getImage(),new Point(5,5), new DnDTransferable(), new DnDSourceListener()); e.startDrag(cursor, new DnDTransferable(), new DnDSourceListener()); } catch (InvalidDnDOperationException idoe) { idoe.printStackTrace(); } } } /** * Inner class DnDTransferable implements Transferable. * 1. Specify the data type we want to transfer. * 2. Also specify the getTransferData(DataFloavor flavor) that will be invoked in target's drop. */ class DnDTransferable implements Transferable { DataFlavor FLAVOR = new DataFlavor(DataFlavor.javaRemoteObjectMimeType, "javaRemoteObjectMimeType"); DataFlavor[] flavors = {FLAVOR}; public Object getTransferData(DataFlavor flavor) { System.out.println("Enter Transferable"); if (flavor.isMimeTypeEqual(DataFlavor.javaRemoteObjectMimeType)) { System.out.println("True: This is an javaRemoteObjectMimeType"); return DnDTabbedPane.this; } else { System.out.println("False: This is null"); return null; } } public DataFlavor[] getTransferDataFlavors() { return flavors; } public boolean isDataFlavorSupported(DataFlavor flavor) { return flavor.isMimeTypeEqual(DataFlavor.javaRemoteObjectMimeType); } } /** * Inner class DnDSourceListener implements DragSourceListener: * All actions when this panel * is treated as a source while dragging */ class DnDSourceListener implements DragSourceListener { public void dragEnter(DragSourceDragEvent e) { Cursor cursor = getToolkit().createCustomCursor(_iconChrome.getImage(), new Point(0,0), "usr"); e.getDragSourceContext().setCursor(cursor); //e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop); } public void dragExit(DragSourceEvent e) { Cursor cursor = getToolkit().createCustomCursor(_iconCloseRed.getImage(), new Point(0,0), "usr"); e.getDragSourceContext().setCursor(cursor); //e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); } public void dragOver(DragSourceDragEvent e) { /*Point tabPt = e.getLocation(); SwingUtilities.convertPointFromScreen(tabPt, DnDTabbedPane.this); int currentIndex = indexAtLocation(tabPt.x, tabPt.y); if(currentIndex<0) { System.out.println(currentIndex + "out of index"); Cursor cursor = getToolkit().createCustomCursor(_iconCloseRed.getImage(), new Point(0,0), "usr"); e.getDragSourceContext().setCursor(cursor); } else { Cursor cursor = getToolkit().createCustomCursor(_iconChrome.getImage(), new Point(0,0), "usr"); e.getDragSourceContext().setCursor(cursor); }*/ } public void dragDropEnd(DragSourceDropEvent e) { } public void dropActionChanged(DragSourceDragEvent e) { } } /** * Target Inner class DnDTargetListener: All actions when this panel * is treated as a target while dragging. */ class DnDTargetListener implements DropTargetListener { public void dragEnter(DropTargetDragEvent ev) { //Get the panel size of the current target. Object obj = ev.getSource(); Component targetComp = ((DropTarget) obj).getComponent(); System.out.println("Current dropPanel size: " + targetComp.getWidth() + "," + targetComp.getHeight()); System.out.println("Drag enter target"); targetAcceptDrag(ev); } public void dragExit(DropTargetEvent ev) { } public void dragOver(DropTargetDragEvent ev) { Object target = ev.getSource(); boolean targetIsJTabbedPane = ((DropTarget) target).getComponent() instanceof JTabbedPane; if (targetIsJTabbedPane) { targetAcceptDrag(ev); } // System.out.println("(X,Y):" + ev.getLocation().x + "," + ev.getLocation().y); } public void dropActionChanged(DropTargetDragEvent ev) { targetAcceptDrag(ev); } public void drop(DropTargetDropEvent ev) { try { Object target = ev.getSource(); if (!bTargetIsTab(target)) { return; } JTabbedPane srcTabbedPane; String title; Component component; DataFlavor[] flavors = ev.getTransferable().getTransferDataFlavors(); System.out.println(flavors.length + " Flavor: " + flavors[0].toString()); JTabbedPane destTabbedPane = (JTabbedPane) ((DropTarget) target).getComponent(); //BasicTabbedPaneUI tabbedPaneUI = new BasicTabbedPaneUI (jtp); for (int i = 0; i < flavors.length; i++) { if (flavors[i].isMimeTypeEqual(DataFlavor.javaRemoteObjectMimeType))// Accept tabs { System.out.println("<<Accept Tabs...>>"); // Get source tab's title and selectedComponent, add to the target // JTabbedPane Object obj = ev.getTransferable().getTransferData(flavors[i]); if(! (obj instanceof JTabbedPane)) { return; } srcTabbedPane = (JTabbedPane) obj; title = getTitle(srcTabbedPane); Icon icon = getIcon(srcTabbedPane); component = srcTabbedPane.getSelectedComponent(); //Move original tab to the destination. destTabbedPane.addTab(title,icon, component); destTabbedPane.setSelectedIndex(destTabbedPane.getTabCount() - 1); //If setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); we have to consider the condition that //after adding tab to a lot of tabs, it has to auto scroll to the new added selected tab //destTabbedPane.scrollRectToVisible(getSelectedTabBound(destTabbedPane)); //destTabbedPane.gett.scrollRectToVisible(getBounds()); } else if (flavors[i].isFlavorSerializedObjectType())// Accept the tree nodes { ImageIcon nodeIcon = new ImageIcon("IDE.JPG"); System.out.println("<<Accept TreeNode...>>"); Object obj = ev.getTransferable().getTransferData(flavors[i]); if(!(obj instanceof ArrayList)) { return; } ArrayList<TreePath> treeList = new ArrayList<TreePath>((ArrayList<TreePath>) obj); for (int j = 0; j < treeList.size(); j++) { destTabbedPane.addTab(treeList.get(j).getLastPathComponent().toString(), nodeIcon, new TextArea(treeList.get(j).toString())); } destTabbedPane.setSelectedIndex(destTabbedPane.getTabCount() - 1); } } } catch (Exception ex) { ex.printStackTrace(); } ev.dropComplete(true); repaint(); } //Methods help to drop /** * Accept the drag */ private void targetAcceptDrag(DropTargetDragEvent ev) { ev.acceptDrag(ev.getDropAction()); } private boolean bTargetIsTab (Object target) { return ((DropTarget) target).getDropTargetContext().getComponent() instanceof JTabbedPane; } /** * Get tab component's title from original tab * @param JTabbedPane * @return String */ private String getTitle(JTabbedPane tabbedPane) { return ((TabComponent)tabbedPane.getTabComponentAt(tabbedPane.getSelectedIndex())).getTitle(); } /** * Get tab component's Icon from original tab * @param JTabbedPane * @return Icon */ private Icon getIcon(JTabbedPane tabbedPane) { return ((TabComponent)tabbedPane.getTabComponentAt(tabbedPane.getSelectedIndex())).getIcon(); } } //Component to be used as tab component of JTabbedPane. /** * Component to be used as tab component of TabbedPane. It contains two JLabel to show the text * and icon and a JButton to close the tab it belongs to */ class TabComponent extends JPanel { private static final long serialVersionUID = 2955071016071608002L; private JButton _button; private JLabel _labelTitle; private JLabel _labelIcon; private int _statusCloseButton = -1; public TabComponent(final String tabTitle, final Icon icon) { super(); initComponents(tabTitle, icon); } /** * Get the icon of the icon label. * @return */ public Icon getIcon() { return _labelIcon.getIcon(); } /** * Get the value of title label. * @return */ public String getTitle() { return _labelTitle.getText(); } /** * Update the value of title label. */ public void setTitle(String text) { _labelTitle.setText(text); } /** * Update the status of close button. */ public void setCloseButtonStatus(int status) { if (_statusCloseButton != status) { setButtonIcon(status); _statusCloseButton = status; } } /** * Update the close button's icon directly. * BUTTON_HOT: Mouse is on the button. * BUTTON_NORMAL: Mouse is on the tab header but not on the button. * BUTTON_NULL: : Mouse is not on the tab header and button. */ private void setButtonIcon(int buttonStatus) { if (_button != null) { // If icon is same as previous, JDK will not reset its icon. switch (buttonStatus) { /*case BUTTON_HOT: _button.setIcon(_iconCloseRed); break;*/ case BUTTON_NORMAL: _button.setIcon(_iconCloseBlack); break; case BUTTON_NULL: _button.setIcon(null); } } } /** * Init components supposed to be on the tab * @param tabTitle * @param icon */ private void initComponents(final String tabTitle, final Icon icon) { setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); // set the component to be transparent. setOpaque(false); setBorder(new EmptyBorder(0, 0, 0, 0)); // make tab icon if (icon != null) { _labelIcon = new JLabel(); _labelIcon.setPreferredSize(new Dimension(19, 19)); _labelIcon.setIcon(icon); add(_labelIcon); add(Box.createRigidArea(new Dimension(5, 5))); } // make tab title _labelTitle = new JLabel(tabTitle); add(_labelTitle); add(Box.createRigidArea(new Dimension(5, 5))); // make close button if (_bClosed) { _button = new JButton(); _button.setBorderPainted(false); _button.setFocusable(false); // make it to be transparent _button.setContentAreaFilled(false); _button.setPreferredSize(new Dimension(19, 19)); _button.setMargin(new Insets(0, 0, 0, 0)); _button.setRolloverIcon(_iconCloseRed); _button.setToolTipText("Close"); _button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { clickCloseButton(e, indexOfTabComponent(TabComponent.this)); } }); _button.addMouseListener(new MouseAdapter() { @Override public void mouseReleased(MouseEvent evt) { // If user right/middle click the mouse, we should convert mouse event to // the TabbedPane to change selection. If user left click the mouse, we do // not need to convert mouse event to the TabbedPanel because the // actionListener of Button will be executed to close tab. if (evt.getButton() != MouseEvent.BUTTON1) convertMouseEventToTabbedPane(_button, evt); } @Override public void mousePressed(MouseEvent evt) { // If user right/middle click the mouse, we should convert mouse event to // the TabbedPane to change selection. If user left click the mouse, we do // not need to convert mouse event to the TabbedPanel because the // actionListener of Button will be executed to close tab. if (evt.getButton() != MouseEvent.BUTTON1) convertMouseEventToTabbedPane(_button, evt); } }); _button.addMouseMotionListener(new MouseAdapter() { public void mouseMoved(MouseEvent evt) { // convert the mouse move event to the the CLTabbedPane, it is used to keep the // icon to be hot. convertMouseEventToTabbedPane(_button, evt); } }); add(_button); } } } /** * Dispatch event from specified component to current DnDTabbedPane. */ private void convertMouseEventToTabbedPane(Component comp, MouseEvent e) { MouseEvent evt = SwingUtilities.convertMouseEvent(comp, e, this); dispatchEvent(evt); } /** * This method is used to handle the click event on the close button. */ protected void clickCloseButton(ActionEvent e, int tabClosedIndex) { remove(tabClosedIndex); } }