시소당
2003/3/18 Tech Tip "Dragging Text and Images with Swing" 에서는 여러 스윙 컴포넌트간에 텍스트와 이미지들의 드래그와 드롭(drag&drop)이 가능하도록 프로그램에 이를 지원하는 방법을 소개했다. 하지만 지난 팁에서는 컴포넌트가 수락(accept)하는 것들과 드롭된 물체를 처리하는 방법을 이미 알고 있었다. 그렇지만 그러한 정보를 알고 있지 못하면 어떨까? 이 글이 후자의 케이스를 설명한다.
3월의 팁에서 javax.swingTransferHandler 클래스를 소개하면서 TransferHandler 객체와 관련된 모든 스윙 컴포넌트들은 자동적으로 드롭될 대상을 받을 수 있다고 했다. 그때는 디폴트 TransferHandler 객체를 사용했었다. 이번 팁에서는, TransferHandler객체를 사용자 정의하는데, 이 때, 어떤 타입의 객체가 컴포넌트에 드롭될 수 있는지 지정하고 드롭이 가능한 이벤트에서 충족되어야 할 것들을 명시해야 한다.
첫번째로 텍스트를 드래그하고 드롭할 수 있게끔 하는 Drop프로그램으로 이야기를 시작해 보자. Drop은 JEditorPane 를 포함하는 JFrame를 생성하고, 이를 실행하면, JEditorPane 에 텍스트를 입력할 수가 있다. 그리고 나면 텍스트를 선택하여 드래그하고 해제(release)하는 것이 가능해진다. 또한 다른 애플리케이션으로부터 텍스트를 선택해와서 텍스트 입력부분에 그것을 드래그할 수도 있다. 텍스트를 선택하고 드래그하면, 아이템을 드래그하고 있다는 것을 표시하는 친숙한 사각형 표시를 볼 수 있다. 또한 타겟 플러스 사인으로 드롭이 가능한 지역을 판단할 수도 있다.
import javax.swing.text.JTextComponent;
import javax.swing.JFrame;
import javax.swing.JEditorPane;
import javax.swing.JButton;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.BorderLayout;
class Drop extends JFrame {
private JTextComponent jText;
public Drop() {
super("Drag n Drop Demo");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300, 400);
createTextArea();
createClearButton();
}
private void createTextArea() {
jText = new JEditorPane();
getContentPane().add(
jText, BorderLayout.CENTER);
}
private void createClearButton() {
JButton clear = new JButton("Clear");
clear.addActionListener(new Clearer());
getContentPane().add(
clear, BorderLayout.SOUTH);
}
private class Clearer implements
ActionListener {
public void actionPerformed(ActionEvent e) {
jText.setText("");
}
}
public static void main(String[] args) {
new Drop().setVisible(true);
}
}
자, JEditorPane에 파일을 드래그하고 드롭하는 것을 해보자. JeditorPane은 드롭된 파일을 수락하지 않기 때문에 이러한 속성(behavior)을 바꾸고자 한다면 custom TransferHandler를 만들어야 한다. 만드는 방법은 TransferHandler클래스에 importData()와 canImport()메소드를 오버라이드하는 것이다. canImport() 메소드는 JComponent가 수락하는 것들을 보여주는 메소드인데, 가령, 다음과 같은 예에서는 JComponent가 JComponent에 드롭된 모든 것들을 수락하고 있다.
public boolean canImport(JComponent c,
DataFlavor[] flavors) {
return true;
}
importData() 메소드는 드롭에 대한 응답으로 동작이 실행되는 장소이고 주로 JComponent에 드롭된 Transferable 객체와 상호작용한다.
다음은 사용자정의된 TransferHandler, DropHandler 이다. canImport()메소드는 컴포넌트에 대한 드롭의 수를 단순히 계산한다.
import javax.swing.TransferHandler;
import javax.swing.JComponent;
import javax.swing.text.JTextComponent;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.DataFlavor;
class DropHandler extends TransferHandler {
private int numberOfDrops;
public boolean importData(JComponent component,
Transferable transferable){
JTextComponent source =
(JTextComponent) component;
source.setText(
"Drop Number: " + ++numberOfDrops);
return true;
}
public boolean canImport(JComponent c,
DataFlavor[] flavors) {
return true;
}
}
위의 예를 실행하려면, 대상 컴포넌트에 TransferHandler를 연결해야 하는데, 모든 JComponents는 setTransferHandler()메소드를 통해서 TransferHandler 을 부를 수가 있다. 다음과 같이 이 호출을 Drop.java 의 createTextArea()메소드에 추가한다.
private void createTextArea() {
jText = new JEditorPane();
getContentPane().add(jText, BorderLayout.CENTER);
jText.setTransferHandler(new DropHandler());
}
이 예제를 실행하기 전에 2가지 중요한 사항을 기억해야 한다. 하나는 컴포넌트의 디폴트 TransferHandler 를 오버라이드하면, 디폴트 속성이 더이상 유지되지 않는다는 것이다. 예를 들어 JEditorPane에 String를 드롭하면, 예상하는대로 String 이 나타나지 않을 수 있다. 다른 하나는, canImport()을 항상 트루(true)로 리턴되도록 오버라이드 해놓았기 때문에, JEditorPane 는 어떤 드롭이던지 수락한다는 것이다. 하지만 어디에서 드래그했느냐에 따라서, 소스 컴포넌트의 데이터를 잃을 수도 있다. 가령, 텍스트 에디터로부터 드래그한 데이더는 성공적으로 드롭을 하더라도 그 에디터로부터 삭제된다는 것이다.
TransferHandler를 File 객체만 수락하도록 사용자정의 해보자. 이를 위해, 3월의 팁에서 소개된 DataFlavors의 개념을 적용해 볼 수가 있다. 전송 중인 객체는 객체가 지원하는 모든 DataFlavor 객체들의 배열을 갖는다. canImport() 메소드를 다음과 같이 변경함으로써 지원을 받는 DataFlavors 중의 하나가 File 인지 아닌지를 테스트해 볼 수가 있다.
public boolean canImport(JComponent c,
DataFlavor[] flavors) {
return Arrays.asList(flavors).
contains(DataFlavor.javaFileListFlavor);
}
public void drop(DropTargetDropEvent dtde) {
Transferable transferable =
dtde.getTransferable();
if (transferable.isDataFlavorSupported(
DataFlavor.javaFileListFlavor)) {
dtde.acceptDrop(DnDConstants.ACTION_COPY);
List files = null;
try {
files =
(List) transferable.getTransferData(
DataFlavor.javaFileListFlavor);
} //exceptions omitted here
for (int i = 0; i < files.size(); i++) {
File file = (File) files.get(i);
jText.setText(
jText.getText() + "\n" + file);
}
}
}
DataFlavor 객체의 배열을 List로 변환해서 리스트가 DataFlavor.javaFileListFlavor 를 포함하고 있는지 여부를 알 수가 있다. JEditorPane 에 드롭되는 File을 처리하기 위해서는, importData() 메소드 또한 변경해야 한다. 이 메소드를 이용하면 드롭되는 객체가 File인지 아닌지를 알아낼 수가 있다. 만약 객체가 File이 아니라면 false를 리턴하고, File이면 이를 처리할 수 있을 것이다. 적절한 예외처리를 해줘야 함은 물론이다.
파일을 다루는 실제적인 코드는 importFiles()메소드이다. 다음의 예에서 DropHandler 의 업데이트된 버전인 importFiles() 메소드는 드롭된 File 의 경로를 각각의 라인에 나타낸다. DropHandler 는 다음과 같다.
import javax.swing.TransferHandler;
import javax.swing.JComponent;
import javax.swing.text.JTextComponent;
import java.util.List;
import java.util.Arrays;
import java.io.File;
import java.io.IOException;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
class DropHandler extends TransferHandler {
public boolean importData(JComponent component,
Transferable transferable) {
if (!canImport(component,
transferable.getTransferDataFlavors())) {
return false;
} else {
JTextComponent source =
(JTextComponent) component;
try {
importFiles(transferable, source);
return true;
} catch (UnsupportedFlavorException e) {
source.setText(e.getMessage());
} catch (IOException e) {
source.setText(e.getMessage());
}
return false;
}
}
private void importFiles(Transferable transferable,
JTextComponent source)
throws UnsupportedFlavorException, IOException {
List files =
(List) transferable.getTransferData(
DataFlavor.javaFileListFlavor);
for (int i = 0; i < files.size(); i++) {
source.setText(source.getText() + "\n"
+ (File)files.get(i));
}
}
public boolean canImport(JComponent c,
DataFlavor[] flavors) {
return Arrays.asList(flavors).
contains(DataFlavor.javaFileListFlavor);
}
}
Drop를 실행하면, JEditorPane 는 오직 파일만을 수락할 것이다. 사용자는 한번에 원하는 만큼의 파일을 드롭할 수 있고, 각각의 라인에 나타나는 경로를 볼 수 있다.
코드의 최종 버전은 제임스 고슬링의 java.net blog entry "URLs are your friend"를 따른다. 제임스 고슬링은 드래그와 드롭을 이용할 때 URL로 변환되도록 하는 것을 선호한다고 한다. 이 마지막 예에서, JEditorPane 에 드롭된 파일들의 경로를 보여주는 대신, 파일로부터 구성된 URLs이 디스플레이 된다.
고슬링은 또한 몇몇 브라우저와 애플리케이션들은 Files을 드롭하려고 할 때 Strings을 드롭한다고 지적하고 있는데, DropHandler의 이번 버전은 canImport()를 재계산하고, 헬퍼 메소드인 canImportFiles() 과 canImportStrings()을 도입함으로써 Files 또는 Strings을 수락한다.
public boolean canImport(JComponent c,
DataFlavor[] flavors) {
return canImportFiles(flavors) ||
canImportStrings(flavors);
}
private boolean canImportFiles(
DataFlavor[] flavors) {
return Arrays.asList(flavors).
contains(DataFlavor.javaFileListFlavor);
}
private boolean canImportStrings(
DataFlavor[] flavors) {
return Arrays.asList(flavors).
contains(DataFlavor.stringFlavor);
}
업데이트된 아래의 DropHandler 에서는, importFiles() 메소드가 경로대신 URL을 디스플레이하도록 변경되었고, importStrings()메소드가 첨가되었다.
private void importStrings(Transferable transferable,
JTextComponent source)
throws UnsupportedFlavorException, IOException {
String string =
(String) transferable.getTransferData(
DataFlavor.stringFlavor);
String message = null;
File file = new File(string);
if (file.exists()) {
message = file.toURL().toString();
} else {
try {
URL url = new URL(string);
url.getContent();
message = url.toString();
} catch (IOException e) {
message = "Could not convert string to URL "
+ string;
}
}
source.setText(source.getText() + "\n" + message);
}
importStrings()메소드는 String으로부터 File을 구성하려 한다. 만약 File이 존재하면 메소드는 File의 toURL()메소드를 이용해서 URL을 구성하지만, File이 존재하지 않으면 importStrings()는 String으로부터 URL을 직접 구성하려고 할 것이다. 그러면 메소드는 URL에 첨부된 리소스가 있는지를 확인한다. 웹 브라우저로부터 이미지를 드래그했을 때, DropHandler는 이것이 가능할 경우에는 적절하게 행동하겠지만, 불가능할 경우에는 에러를 낼 것이다.
importData() 메소드는 단순화될 수도 있다. 이 메소드는 전송된 객체가 File 인지 String인지를 테스트한 후, 적절한 메소드를 호출한다. 재작성된 DropHandler 를 보자.
import javax.swing.TransferHandler;
import javax.swing.JComponent;
import javax.swing.text.JTextComponent;
import java.util.List;
import java.util.Arrays;
import java.io.File;
import java.io.IOException;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.net.URL;
class DropHandler extends TransferHandler {
public boolean importData(JComponent component,
Transferable transferable) {
DataFlavor[] flavors =
transferable.getTransferDataFlavors();
JTextComponent source =
(JTextComponent) component;
try {
if (canImportFiles(flavors)) {
importFiles(transferable, source);
} else if (canImportStrings(flavors)) {
importStrings(transferable, source);
} else
return false;
return true;
} catch (UnsupportedFlavorException e) {
source.setText(e.getMessage());
} catch (IOException e) {
source.setText(e.getMessage());
}
return false;
}
private void importFiles(Transferable transferable,
JTextComponent source)
throws UnsupportedFlavorException, IOException {
List files =
(List) transferable.getTransferData(
DataFlavor.javaFileListFlavor);
for (int i = 0; i < files.size(); i++) {
source.setText(source.getText() + "\n" +
((File) (files.get(i))).toURL());
}
}
private void importStrings(
Transferable transferable,
JTextComponent source)
throws UnsupportedFlavorException, IOException {
String string =
(String) transferable.getTransferData(
DataFlavor.stringFlavor);
String message = null;
File file = new File(string);
if (file.exists()) {
message = file.toURL().toString();
} else {
try {
URL url = new URL(string);
url.getContent();
message = url.toString();
} catch (IOException e) {
message = "Could not convert string to URL "
+ string;
}
}
source.setText(
source.getText() + "\n" + message);
}
public boolean canImport(JComponent c,
DataFlavor[] flavors) {
return canImportFiles(flavors) ||
canImportStrings(flavors);
}
private boolean canImportFiles(
DataFlavor[] flavors) {
return Arrays.asList(flavors).
contains(DataFlavor.javaFileListFlavor);
}
private boolean canImportStrings(
DataFlavor[] flavors) {
return Arrays.asList(flavors).
contains(DataFlavor.stringFlavor);
}
}
이 팁은 일반적인 JTextComponent를 위한 디폴트 TransferHandler로 시작했다. 그리고는 제한적으로 DataFlavor를 취할 수 있는 TransferHandler를 보여주고, Files와 Strings를 수락하기 위해 TransferHandler를 변경하는 방법과 유용한 URLs를 생성하는 방법을 설명했다. 두 가지 예 모두에서 canImport() 와 importData() 메소드를 필요에 맞게 조정해야 한다.
TransferHandler와 데이터 전송에 대한 좀 더 자세한 정보는 자바 튜토리얼의 "How to Use Data Transfer"를 참고한다.
출처 TRANSFERHANDLER 사용하기|작성자 쭌
http://blog.naver.com/imcho57?Redirect=Log&logNo=40008079698