用Java Swing实现可以拖拽tab的JTabbedPane

下面的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);
	}
}


 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值