Trong lập trình GUI thì Drag và Drop (tiếng việt là Kéo và Thả) là hành động click và giữ chuột lên một đối tượng rồi “kéo” đối tượng đó lên một vị trí khác hoặc lên một đối tượng khác.
Kéo thả là một trong những tính năng thường thấy trong GUI, cho phép người dùng thực hiện các công việc phức tạp.
Chúng ta có thể kéo thả dữ liệu hoặc các đối tượng có hình thù cụ thể, ví dụ như chúng ta kéo một file ảnh vào cửa sổ chat để gửi file thì đó là kéo dữ liệu, hoặc kéo các tab trong trình duyệt Chrome là kéo đối tượng có hình thù.
Hầu hết các component trong Java Swing đều có phương thức hỗ trợ kéo thả, và chúng ta cũng có thể viết các phương thức kéo thả của riêng chúng ta.
Ví dụ
import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JTextField; import javax.swing.TransferHandler; public class Example extends JFrame { JTextField field; JButton button; public Example() { setTitle("Drag And Drop Example"); setLayout(null); button = new JButton("Button"); button.setBounds(200, 50, 90, 25); field = new JTextField(); field.setBounds(30, 50, 150, 25); add(button); add(field); field.setDragEnabled(true); button.setTransferHandler(new TransferHandler("text")); setSize(330, 150); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setVisible(true); } public static void main(String[] args) { new Example(); } }
Chúng ta hiển thị một JTextField
và một JButton
, đoạn text trong text field có thể kéo sang làm text cho button.
field.setDragEnabled(true);
Để dữ liệu của một component có thể kéo được thì chúng ta phải thiết lập trong phương thức setDragEnabled()
vì mặc định Swing đã vô hiệu hóa tính năng này.
button.setTransferHandler(new TransferHandler("text"));
Lớp TransferHandler
là trái tim của Drag và Drop trong Swing, lớp này có nhiệm vụ vận chuyển dữ liệu qua lại giữa các component, tham số của lớp này là một thuộc tính Bean, nếu bạn chưa biết gì về các thuộc tính bean thì chỉ cần nhớ rằng bất kì thuộc tính nào có phương thức getter và setter đều có thể đưa vào làm tham số cho TransferHandler.
Chẳng hạn như JButton
có phương thức getText()
và setText()
, do đó bạn có thể đưa tham số là "text"
,
vì khi chúng ta thiết lập kiểu dữ liệu là "text"
, TransferHandler
sẽ dùng các phương thức getter và setter tương ứng để lấy và nhập dữ liệu với các component. Và vì chúng ta thiết lập kiểu text nên bạn chỉ có thể kéo hoặc thả các chuỗi text vào ra component này thôi.
Tùy chỉnh khả năng kéo
Không phải tất cả các component trong Swing đều có thể kéo được, JLabel
là một ví dụ, chúng ta phải code phương thức kéo riêng. Ở đây chúng ta sẽ thực hiện kéo thả thuộc tính icon
.
import java.awt.FlowLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.TransferHandler; public class Example extends JFrame { public Example() { setTitle("Drag And Drop Example"); JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 50, 15)); ImageIcon icon1 = new ImageIcon("C:/sad.png"); ImageIcon icon2 = new ImageIcon("C:/smile.png"); ImageIcon icon3 = new ImageIcon("C:/crying.png"); JButton button = new JButton(icon2); button.setFocusable(false); JLabel label1 = new JLabel(icon1, JLabel.CENTER); JLabel label2 = new JLabel(icon3, JLabel.CENTER); MouseListener listener = new DragMouseAdapter(); label1.addMouseListener(listener); label2.addMouseListener(listener); label1.setTransferHandler(new TransferHandler("icon")); button.setTransferHandler(new TransferHandler("icon")); label2.setTransferHandler(new TransferHandler("icon")); panel.add(label1); panel.add(button); panel.add(label2); add(panel); pack(); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setVisible(true); } class DragMouseAdapter extends MouseAdapter { public void mousePressed(MouseEvent e) { JComponent c = (JComponent) e.getSource(); TransferHandler handler = c.getTransferHandler(); handler.exportAsDrag(c, e, TransferHandler.COPY); } } public static void main(String[] args) { new Example(); } }
Chúng ta hiển thị 2 label và một button, cả 3 component này đều hiển thị icon, chúng ta có thể kéo icon từ 2 label vào làm icon cho button.
MouseListener listener = new DragMouseAdapter(); label1.addMouseListener(listener); label2.addMouseListener(listener);
Như đã nói, JLabel
không được hỗ trợ tính năng kéo, hay nói cách khác là không có phương thức setDragEnabled()
, nên ở đây chúng ta tự gắn một MouseAdapter
vào để mô phỏng sự kiện kéo.
label1.setTransferHandler(new TransferHandler("icon")); button.setTransferHandler(new TransferHandler("icon")); label2.setTransferHandler(new TransferHandler("icon"));
Cả 3 component của chúng ta đều có các phương thức getter/setter cho thuộc tính icon. Đối với các lớp có sẵn phương thức hỗ trợ kéo là setDragEnabled()
thì bạn có thể không cần phải chỉ ra kiểu dữ liệu vận chuyển trong TransferHandler
và Swing sẽ vận chuyển các dữ liệu mặc định (chẳng hạn như text đối với JTextField
), còn với các lớp không có sẵn thì chúng ta phải chỉ ra kiểu dữ liệu rõ ràng trong phương thức setTransferHandler()
.
JComponent c = (JComponent) e.getSource(); TransferHandler handler = c.getTransferHandler(); handler.exportAsDrag(c, e, TransferHandler.COPY);
Ba dòng code trên thiết lập khả năng kéo cho JLabel
bằng phương thức exportAsDrag()
, phương thức này nhận vào đối tượng được kéo đi (e.getSource()
), dữ liệu về sự kiện kéo chuột (
e)
và cách mà dữ liệu được, ở đây là TransferHandler.COPY
, tức là dữ liệu từ component nguồn sẽ được copy sang đối tượng đích. Ngoài copy thì chúng ta còn có một số thao tác khác như MOVE
, NONE
, MOVE_OR_COPY
, COPY
, LINK
.
Tùy chỉnh khả năng thả
Nếu có một số component không có sẵn phương thức hỗ trợ kéo thì cũng có một số component không có phương thức hỗ trợ thả, JList
là một ví dụ. Lý do là bởi vì khi chúng ta kéo thả vào JList
thì Swing không biết chúng ta muốn thả vào như thế nào, vd như insert vào đầu list, cuối list hay giữa list hay ở vị trí bất kì nào…
import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import javax.swing.DefaultListModel; import javax.swing.DropMode; import javax.swing.JFrame; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.TransferHandler; public class Example extends JFrame { JTextField field; DefaultListModel model; public Example() { setTitle("Drag And Drop Example"); JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 15)); JScrollPane pane = new JScrollPane(); pane.setPreferredSize(new Dimension(180, 150)); model = new DefaultListModel(); JList list = new JList(model); list.setDropMode(DropMode.INSERT); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setTransferHandler(new ListHandler()); field = new JTextField(""); field.setPreferredSize(new Dimension(150, 25)); field.setDragEnabled(true); panel.add(field); pane.getViewport().add(list); panel.add(pane); add(panel); pack(); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setVisible(true); } private class ListHandler extends TransferHandler { public boolean canImport(TransferSupport support) { if (!support.isDrop()) { return false; } return support.isDataFlavorSupported(DataFlavor.stringFlavor); } public boolean importData(TransferSupport support) { if (!canImport(support)) { return false; } Transferable transferable = support.getTransferable(); String line; try { line = (String) transferable.getTransferData(DataFlavor.stringFlavor); } catch (Exception e) { return false; } JList.DropLocation dl = (JList.DropLocation) support.getDropLocation(); int index = dl.getIndex(); String[] data = line.split(","); for (String item: data) { if (!item.isEmpty()) model.add(index++, item.trim()); } return true; } } public static void main(String[] args) { new Example(); } }
Chúng ta sử dụng một JTextField
và một JList
, các đoạn text trong JTextField
có thể được kéo qua JList
, đoạn text sẽ dược phân ra thành nhiều text con bằng dấu phẩy.
list.setDropMode(DropMode.INSERT);
Phương thức setDropMode()
thiết lập cách dữ liệu được thả vào, ở đây là DropMode.INSERT
, tức là chèn vào cuối list.
list.setTransferHandler(new ListHandler());
Mặc dù chúng ta chỉ kéo thả các đoạn text bình thường từ JTextField
sang nhưng vì JList
không có các phương thức getter/setter tương ứng nên chúng ta phải định nghĩa một lớp TransferHandler
riêng là ListHandler
.
public boolean canImport(TransferSupport support) { if (!support.isDrop()) { return false; } return support.isDataFlavorSupported(DataFlavor.stringFlavor); }
Khi chúng ta kéo dữ liệu sang một component (chỉ kéo thôi chứ chưa thả), phương thức canImport()
của đối tượng TransferHandler
đó sẽ được gọi liên tục, phương thức này nhận một đối tượng TransferSupport
do Swing tự tạo ra, phương thức này sẽ kiểm tra xem dữ liệu được chuyển sang có được nhận hay không. Chúng ta override lại phương thức này trong lớp ListHandler
.
Ở đây chúng ta dùng phương thức isDrop()
kiểm tra xem người dùng đã thả dữ liệu ra chưa hay vẫn còn giữ chuột để tiếp tục kéo. Phương thức isDataFlavorSupported()
kiểm tra xem dữ liệu được chuyển sang có được hỗ trợ hay không, phương thức này nhận vào một đối tượng lớp DataFlavor
, lớp này lưu trữ thông tin về các loại dữ liệu khác nhau, ở đây DataFlavor.stringFlavor
lưu thông tin về kiểu string, ngoài ra còn một số kiểu khác như imageFlavor
, allHtmlFlavor
… bạn có thể tìm hiểu thêm tại đây.
public boolean importData(TransferSupport support) { ... }
Phương thức importData()
trong lớp TransferHandler
sẽ được gọi khi người dùng thả chuột ra, tức là thả dữ liệu lên component, phương thức này nhận một đối tượng TransferSupport
do Swing tự tạo ra, chúng ta cũng override lại phương thức này.
Transferable transferable = support.getTransferable();
Bên trong đối tượng TransferSupport
có chứa một đối tượng Transferable
, đây là đối tượng chứa thông tin về dữ liệu được vận chuyển.
line = (String) transferable.getTransferData(DataFlavor.stringFlavor);
Để lấy dữ liệu text thì chúng ta dùng phương thức getTransferData()
và đưa vào tham số DataFlavor
tương ứng.
JList.DropLocation dl = (JList.DropLocation) support.getDropLocation(); int index = dl.getIndex();
Bên trong đối tượng TranferSupport
còn chứa một đối tượng TransferHandler.DropLocation
, chúng ta lấy ra từ phương thức getDropLocation()
, lớp này chứa thông tin về tọa độ chuột mà dữ liệu được thả.
Lớp JList
cũng có một lớp DropLocation
riêng kế thừa từ lớp này, JList.DropLocation
sẽ dựa vào tọa độ được thả ra mà tính vị trí chỉ số trong danh sách các item, chúng ta lấy chỉ số này thông qua phương thức getIndex()
.
String[] data = line.split(","); for (String item: data) { if (!item.isEmpty()) model.add(index++, item.trim()); }
Cuối cùng chúng ta phân tích đoạn text được gửi sang rồi insert vào JList
.