2011/02/07

魔法使いの弟子(その3-1)

【ミッション: ファイルのドラッグ & ドロップをサポートしたい!】

今さらながら、O'reilly の “JAVA SWING HACKS” に載っていた HACK #59 『ファイルをドラッグ & ドロップする』 を試してみた。

これは、アプリ画面上部のアイコンをドラッグ & ドロップすると、テキストエリアに入力した内容でファイルが作られるのだ。

アイコンをドラッグ & ドロップすると、その先に「myfile.txt」が作られる。
ファイルの中身はアプリのテキストエリアと同じ。

本に載っている断片的なソースコードを継ぎ合わせると、だいたい以下のようになる。

import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceAdapter;
import java.awt.dnd.DragSourceContext;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextArea;
import javax.swing.SwingConstants;
import javax.swing.filechooser.FileSystemView;

// ドラッグ&ドロップのテスト
public class FileDropper {
    public static void main(String[] args) throws IOException {
        JFrame frame = new JFrame("Drag and Drop File Hack");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        FileSystemView fsv = FileSystemView.getFileSystemView();
        Icon icon = fsv.getSystemIcon(File.createTempFile("myfile.",".txt"));

        frame.getContentPane().setLayout(new BorderLayout());
        JTextArea text = new JTextArea();

        JLabel label = new JLabel("myfile.txt",icon,SwingConstants.CENTER);
        DragSource ds = DragSource.getDefaultDragSource();
        DragGestureRecognizer dgr = ds.createDefaultDragGestureRecognizer(
                label,
                DnDConstants.ACTION_MOVE,
                new FileDragGestureListener(text));

        frame.getContentPane().add("North",label);
        frame.getContentPane().add("Center",text);

        frame.pack();
        frame.setSize(400,300);
        frame.setVisible(true);
    }
}

class FileDragGestureListener extends DragSourceAdapter implements DragGestureListener {
    JTextArea text;
    Cursor cursor;
    
    public FileDragGestureListener(JTextArea text) {
        this.text = text;
    }

    public void dragGestureRecognized(DragGestureEvent evt) {
        try {
            // 一時ファイルを生成
            File temp_dir = File.createTempFile("tempdir",".dir",null);
            File temp = new File(temp_dir.getParent(),"myfile.txt");
            FileOutputStream out = new FileOutputStream(temp);
            out.write(text.getText().getBytes());
            out.close();

            // 適切なアイコンを取得
            FileSystemView fsv = FileSystemView.getFileSystemView();
            Icon icn = fsv.getSystemIcon(temp);

            Toolkit tk = Toolkit.getDefaultToolkit();
            Dimension dim = tk.getBestCursorSize(icn.getIconWidth(),icn.getIconHeight());
            BufferedImage buff = new BufferedImage(dim.width,dim.height,BufferedImage.TYPE_INT_ARGB);
            icn.paintIcon(text,buff.getGraphics(),0,0);

            // ドラッグイメージをセットアップ
            if(DragSource.isDragImageSupported()) {
                evt.startDrag(DragSource.DefaultCopyDrop, buff, new Point(0,0),
                        new TextFileTransferable(temp),
                        this);
            } else {
                cursor = tk.createCustomCursor(buff,new Point(0,0),"billybob");
                evt.startDrag(cursor, null, new Point(0,0),
                        new TextFileTransferable(temp),
                        this);
            }

        } catch (IOException ex) {
            /* なんかバグってるよ */
        }
    }

    public void dragEnter(DragSourceDragEvent evt) {
        DragSourceContext ctx = evt.getDragSourceContext();
        ctx.setCursor(cursor);
    }

    public void dragExit(DragSourceEvent evt) {
        DragSourceContext ctx = evt.getDragSourceContext();
        ctx.setCursor(DragSource.DefaultCopyNoDrop);
    }
}

// 一時ファイルを保持するTransferable
class TextFileTransferable implements Transferable {
    File temp;

    public TextFileTransferable(File temp) throws IOException {
        this.temp = temp;
    }

    public Object getTransferData(DataFlavor flavor) {
        List<File> list = new ArrayList<File>();
        list.add(temp);
        return list;
    }

    public DataFlavor[] getTransferDataFlavors() {
        DataFlavor[] df = new DataFlavor[1];
        df[0] = DataFlavor.javaFileListFlavor;
        return df;
    }

    public boolean isDataFlavorSupported(DataFlavor flavor) {
        if(flavor == DataFlavor.javaFileListFlavor) {
            return true;
        }
        return false;
    }
}

このサンプルは、 Java アプリケーションから外部へファイルをドラッグ & ドロップするというものだが、逆に外部からファイルを Java アプリケーションへドロップできたら何かと便利そうである。



例えば、 SD カードから特定の画像だけを『セロテープぺったん画像』に変換したい場合に、ドラッグ & ドロップでアプリにファイルを放り込む事ができたらきっとラクだろう。

というわけで、上記のソースをじゃっかん改造して作ってみた。『アプリから』ドラッグするのと『アプリへ』ドラッグするのとでは、ドラッグまわりの実装方針が異なるが、ファイルのやりとりの抽象化に関しては一緒である。

あと、 GUI 構築に関するコードを中心に、ぼくのスタイルに(勝手に)書き換えている。

作ったのは右下のふざけたアプリ。
テキストエリアにファイルをドラッグ & ドロップすると、フルパスが表示される。
ドロップされた時点でファイル群はリスト List に格納されているので、あとは for ループで一つずつ取り出して処理を行えばよい。

一応ソースコードを晒しておく。

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

/**
 * テキストエリアのところにファイルをドロップすると、
 * その絶対パスの一覧を表示するらしい。
 * @author tercel
 */
public class FilesDropTest extends JFrame implements DropTargetListener {
    public static void main(String args[]) {
        FilesDropTest app = new FilesDropTest();
        app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        app.setSize(400, 300);
        app.setVisible(true);
    }

    DropTarget dropTarget;
    JTextArea textArea; // ここにファイルの一覧を表示するよ!

    // コンストラクタ
    public FilesDropTest() {
        super("テストだっち ∩( ・ω・)∩");

        textArea = new JTextArea();
        JScrollPane scroll = new JScrollPane(textArea);
        this.add(scroll);

        dropTarget = new DropTarget(textArea, this);

    }
    
    // DropTargetListener の各メソッドのオーバーライド
    @Override
    public void dragEnter(DropTargetDragEvent dtde) {}

    @Override
    public void dragOver(DropTargetDragEvent dtde) {}

    @Override
    public void dropActionChanged(DropTargetDragEvent dtde) {}

    @Override
    public void dragExit(DropTargetEvent dte) {}

    @Override
    public void drop(DropTargetDropEvent dtde) {
        dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
        Transferable trans = dtde.getTransferable();

        List<File> fileList = null;

        if(trans.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
            try {
                fileList = (List<File>) trans.getTransferData(DataFlavor.javaFileListFlavor);
            } catch (UnsupportedFlavorException ex) {
                /* 例外処理 */
            } catch (IOException ex) {
                /* 例外処理 */
            }
        }

        /* ここの時点で、fileList にはドロップされたファイルのリストが入ってるよ! */
        /* 煮るなり焼くなりできるね */
        if(fileList == null) return;
        for(File f : fileList)
            textArea.append(f.getAbsolutePath() + "\n");
    }
}

たったこれだけで、ドラッグ & ドロップによるファイルのインポートがサポートできた。

うれしい。