今月はJavaにおける最大のお祭りJavaOneがサンフランシスコで行われます。櫻庭も例年のごとく参加します。せっかく参加するのですから,この連載でもJavaOneをレポートしたいと思います。

そこで,変則的なのですが,今月は今週と第4週をJava SE 6完全攻略,第2週と第4週をJavaOneレポートとさせていただきます。

さて,今月のJava SE 6完全攻略では,Swingのドラッグ&ドロップについて解説します。

Swingのドラッグ&ドロップはJ2SE 1.4から導入された機能です。ただ,Swingのドラッグ&ドロップに関する解説はまだあまりありません。そこでまずはじめに,今までのドラッグ&ドロップについて解説します。その後,Java SE 6でのドラッグ&ドロップの強化点について解説します。

Swingコンポーネントのドラッグ&ドロップのサポート

はじめに,Swingのコンポーネントがドラッグ&ドロップを含めたデータ転送をどのようにサポートしているかを説明しましょう。

表1はSwingチュートリアルのIntroduction to Drag and Drop and Data Transferから引用した表です。

表1 Swingコンポーネントのデータ転送サポート
コンポーネント ドラッグ
コピー
ドラッグ
移動
ドロップ カット コピー ペースト
JColorChooser checked   checked      
JEditorPane checked checked checked checked checked checked
JFileChooser checked       checked  
JFormattedTextField checked checked checked checked checked checked
JList checked       checked  
JPasswordField n/a n/a checked n/a n/a checked
JTable checked       checked  
JTextArea checked checked checked checked checked checked
JTextField checked checked checked checked checked checked
JTextPane checked checked checked checked checked checked
JTree checked       checked  

JTextFieldクラスやJEditorPaneクラスなどの文字列を編集するクラスは,データ転送におけるすべての操作が可能になっています。逆にJTableクラスやJTreeクラスは限られた操作しかできません。また,この表にないJLabelクラスなどはデータ転送が行えません。

このように,コンポーネントによってデータ転送のサポートが異なることがわかります。

手始めに,JTextAreaクラスでドラッグ&ドロップを試してみましょう。

サンプルのソースコード SimpleDnD.java

SimpleDnDクラスは三つのテキストエリアを持ちます(図1)。それぞれのテキストエリアは「ドロップのみ」「ドラッグとドロップの両方が可能」「ドラッグとドロップのどちらも不可」になっています。図1は,各テキストエリアでドロップ動作を行おうとしているところです。一番下のテキストエリアだけドロップできないことがわかります。

SimpleDnDへのドロップ動作
図1 SimpleDnDへのドロップ動作

JTextAreaクラスやJTextFieldクラスといったドラッグとドロップの両方が可能なクラスであっても,デフォルトではドラッグはできないようになっています。

表1でドラッグが可能となっているコンポーネントもデフォルトではドラッグできないのです。

ドラッグを可能にするには,次のようなコードを記述します。

// TextArea をドラッグ可能にする
area2.setDragEnabled(true);

setDragEnabledメソッドは,表1に示したドラッグが可能なコンポーネントで定義されています。

JTextAreaクラスのようにドラッグ&ドロップが可能なクラスに対して,ドラッグ&ドロップを不可にするにはどうすればいいのでしょうか。これも簡単に実現できます。

// TextAreaをドラッグ&ドロップ不可にする
area3.setTransferHandler(null);

setTransferHandlerメソッドの引数をnullにするということは,すでに設定されているTransferHandlerクラスを無効にしてしまうということです。

そう,javax.swing.TransferHandlerクラスがドラッグ&ドロップを実現するためのキーになるクラスなのです。

AWTでドラッグ&ドロップを実装したことがある人はご存じでしょうが,ドラッグ&ドロップで転送されるデータはjava.awt.datatransfer.Transferableインタフェースを実装したクラスで表されます。

AWTでは,DragSourceEventイベントやDragSourceDropEventイベント,DragSourceDragEventイベントを使用し,Transferableオブジェクトを介してドラッグ&ドロップを行います。

なんとなく面倒ですね。でも,Swingではこうした面倒なところをすべてTransferHandlerクラスが行ってくれます。

図2に示したように,ドラッグが開始されるとComponentAに関連付けられたTransferHandlerオブジェクトはComponentAから転送する情報を取得します。そして,Transferableオブジェクトを生成し,そこにComponentAから取得したデータを詰め込みます。

ComponentBにドロップされる場合,ComponentBに関連付けられたTransferableHandlerオブジェクトは,Transferableオブジェクトからデータを取り出し,それをComponentBにセットします。

TransferHandlerクラスとドラッグ&ドロップ
図2 TransferHandlerクラスとドラッグ&ドロップ

TransferHandlerクラスの使い方

それではTransferHandlerクラスを使ってドラッグ&ドロップを実現してみましょう。転送するデータが文字列の場合はとても簡単です。

サンプルとして,ドラッグ&ドロップをサポートしていないJLableクラスを扱ってみましょう。

サンプルのソースコード JLabelDnD.java

JLabelクラスは文字列とアイコンを扱えますが,ここでは文字列だけを対象にします。

JLabel label = new JLabel(TEXT2);

// D&D のためのTransferHandlerオブジェクトをセット
label.setTransferHandler(new TransferHandler("text"));

TransferHandlerオブジェクトをコンポーネントにセットするにはJComponent#setTransferHandlerメソッドを使用します。

TransferHandlerクラスのコンストラクタの引数はデータ転送を行うプロパティ名です。これだけでドロップが可能になります。

次にドラッグできるようにしましょう。ドラッグを実現するには,ドラッグを開始するイベントが発生したときにデータをTransferHandlerオブジェクトに引き渡します。とはいっても,とても簡単。イベント処理で次のように記述します。

// Drag 開始時にデータの交換を行う
label.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent event) {
        JLabel label = (JLabel)event.getSource();
        TransferHandler handler = label.getTransferHandler();
        handler.exportAsDrag(label, event, TransferHandler.COPY);
    }
});

ここでは,マウスでラベルをクリックしたときをドラッグ開始としています。

イベントからソースのコンポーネントを取得し,そこからgetTransferHandlerメソッドでTransferHandlerオブジェクトを取り出します。

次に,exportAsDragメソッドをコールします。exportAsDragメソッドは第1引数がコンポーネント,第2引数が発生したイベント,第3引数がデータ転送の種類です。ここでは第3引数としてCOPYを使用していますが,ほかにはMOVEを使用できます。

実際に転送するデータはTransferHandlerクラスのコンストラクタで指定されているので,exportAsDragメソッドで指定する必要はありません。

これで,JLabelクラスでもドラッグ&ドロップが可能になりました。ぜひ,実行してみてください(図3)。

ただし,ラベルで表示している文字列の一部をドラッグ&ドロップすることはできません。そのような用途にはJTextFieldクラスやJTextAreaクラスを編集不可の状態で使用します。

JLabelDnDの実行結果
a) ドラッグ中
JLabelDnDの実行結果
b) ドロップ
図3 JLabelDnDの実行結果

TransferHandlerクラスを拡張する

もう少し複雑な例を扱ってみましょう。

表1をもう一度見てください。JListクラス,JTableクラス,JTreeクラスなどは,ドラッグはできるもののドロップはできません。これをドロップできるようにしてみましょう。

例えばJListクラスをドロップできるようにした場合,どのように項目を追加するかといったことを決めなくてはなりません。「改行された文字列を別々の項目として扱うかどうか」「HTMLで表されていたら<ul>タグで表されるリストを項目として扱うようにする」といったことです。

このような処理を行うにはTransferHandlerクラスだけではできません。それぞれのクラスに応じてTransferHandlerクラスを派生させたクラスを作成します。

サンプルとしてJListクラスを扱いましょう。

サンプルのソースコード JListDnD.javaListTransferHandler.java

文字列をドロップできるようにし,文字列の中に改行が含まれていれば,別々の項目として扱うことにしました。この処理を行うのがTransferHandlerクラスの派生クラスであるListTransferHandlerクラスです。今回はクリップボードを介したデータ転送(カット&ペーストやコピー&ペースト)は対象からはずし,ドラッグ&ドロップの部分だけを実装しました。

整理のために,TransferHandlerクラスのメソッドをドラッグとドロップに分けてみます。

ドラッグ

  1. exportAsDrag
  2. createTrasferable
  3. exportDone

ドロップ

  1. canImport
  2. importData

exportAsDragメソッドは,JLabelDnDクラスで使ったように,イベント時にコールされます。このメソッドはTransferHandlerクラスで定義されているものをそのまま使用します。次のcreateTransferableメソッドが,Transferableオブジェクトを生成するメソッドです。このメソッドは扱うデータにより処理が異なるので,オーバーライドします。

次のexportDoneメソッドはドラッグが終了したときにコールされるメソッドです。ドラッグ元のコンポーネントの表現によって処理も異なるので,このメソッドもオーバーライドします。

ドロップに関するメソッドは二つ。canImportメソッドは,Transferableオブジェクトが保持しているデータをコンポーネントにドロップできるかどうかを判定します。もう一方のimportDataメソッドは,実際のドロップ処理を記述します。どちらのメソッドもオーバーライドします。

そのほかにオーバーライドするメソッドに,getSourceActionメソッドがあります。このメソッドはコピーと移動のどちらをサポートしているかを返すメソッドです。

それではコードを順番に見ていきましょう。まずはcreateTransferableメソッドからです。

protected Transferable createTransferable(JComponent c) {
    // ドラッグされた部分からTransferableを作成する
    // 複数行選択されていたら,改行を加えた複数の文字列とする
    JList list = (JList)c;
    indices = list.getSelectedIndices();
    Object[] values = list.getSelectedValues();
     
    StringBuilder builder = new StringBuilder();

    for (int i = 0; i < values.length; i++) {
        if (values[i] == null) {
            // 項目データがなければ,空文字を追加
            builder.append("");
        } else {
            builder.append(values[i].toString());
        }

        // 最終行以外は改行を加える
        if (i != values.length - 1) {
            builder.append("\n");
        }
    }
     
    // 文字列を扱うTransferableインタフェースを実装した
    // StringSelectionオブジェクトを生成
    return new StringSelection(builder.toString());
}

行っている処理はそれほど難しいものではありません。リストで選択されている項目から複数行の文字列を作成しています。

キーとなるのが赤字で示したStringSelectionオブジェクトの生成です。java.awt.datatransfer.StringSelectionクラスは,Transferableインタフェースを実装したクラスで,文字列を扱うことができます。

文字列はデータ転送で最もよく使用されるので,標準でTransferableインタフェースを実装したクラスが提供されています。もし,イメージなど,文字列以外のデータを使用する場合には,Transferableインタフェースを実装するクラスを自作する必要があります。

さて,次にexportDoneメソッドです。

protected void exportDone(JComponent c, Transferable data, int action) {
    // 移動であれば,選択された部分を削除する
    if (action == MOVE && indices != null) {
        JList list = (JList)c;
        DefaultListModel model  = (DefaultListModel)list.getModel();
			
        // 同じリスト内の移動であれば,インデックスを再計算する
        if (addCount > 0) {
            for (int i = 0; i < indices.length; i++) {
                if (indices[i] > addIndex) {
                    indices[i] += addCount;
                }
            }
        }

        // 項目の削除
        for (int i = indices.length - 1; i >= 0; i--) {
            model.remove(indices[i]);
        }
    }

    indices = null;
    addCount = 0;
    addIndex = -1;
}

リストの項目選択には「単一」「連続した複数」「不連続な複数」の3種類があるのですが,ここでは簡略化のため,不連続な選択は対象にしていません。

引数のactionにはCOPYもしくはMOVEを指定します。MOVEのときにはドラッグされた項目をモデルから削除します。

indicesはドラッグ時に選択されていた項目のインデックスを保持している配列です。addCountとaddIndexは同じリスト内でドラッグ&ドロップで項目を移動させたときに使用しています。addCountがドロップで追加された項目数,addIndexがドロップされたインデックスを示しています。

同じリスト内で移動させる場合,ドロップの処理が呼び出された後,exportDoneメソッドがコールされます。つまり,ドロップ処理でリストに項目が追加されてからexportDoneメソッドがコールされるため,indicesが示しているドラッグ時のインデックスは,ドロップされた状態に応じて再計算しなければなりません。

再計算した後,リストのモデルからドラッグされた項目を削除します。最後にindicesやaddCount,addIndexをリセットしておきます。

これでドラッグに関する部分は実装できました。次にドロップに関する処理を実装します。

ドロップの際,Transferableオブジェクトが保持しているデータが,ドロップ先のコンポーネントで扱えるかどうかをチェックしなくてはなりません。そのために使用するのがcanImportメソッドです。

public boolean canImport(JComponent c, DataFlavor[] flavors) {
    // 文字列であればドロップ可能とする
    for (DataFlavor flavor: flavors) {
        if (DataFlavor.stringFlavor.equals(flavor)) {
            return true;
        }
    }
		
    return false;
}

Transferableオブジェクトが保持しているデータの種別(フレーバ)はjava.awt.datatransfer.DataFlavorクラスで扱います。canImportメソッドの引数がDataFlavorクラスの配列になっているのは,一つのTransferableオブジェクトを複数のフレーバで表せるからです。

例えば,プレーンな文字列とHTMLの両方で表すといったことができます。

DataFlavorクラスには,よく使用するデータのフレーバが定数として用意されています。文字であればDataFlavor.stringFlavor,イメージであればDataFlavor.imageFlavorです。もちろん,新しいフレーバを作成することもできます。

ここでは文字列を扱うので,引数のフレーバとstringFlavorを比較しています。マッチすればtrueを返し,引数のフレーバが文字列以外であればfalseを返します。

次がドロップ処理の本体であるimportDataメソッドです。

public boolean importData(JComponent c, Transferable t) {
    if (canImport(c, t.getTransferDataFlavors())) {
        try {
            Object obj = t.getTransferData(DataFlavor.stringFlavor);
            String text = (String)obj;
            importString(c, text);

            return true;
        } catch (UnsupportedFlavorException ex) {
            // 失敗したらfalseを返すだけ
        } catch (IOException ex) {
            // 失敗したらfalseを返すだけ
        }
    }

    return false;
}

canImportメソッドを先に説明してしまいましたが,コールされる順番はimportDataメソッドの方が先です。

importDataメソッドの内部でcanImportメソッドをコールし,ドロップできるかどうかを判定します。このときに,Transferableオブジェクトからフレーバを取り出すためにTransferable#getTransferDataFlavorsメソッドを使用します。

次にドロップ処理です。

ドロップするデータは,Transferable#getTransferDataメソッドを使用します。getTransferDataメソッドの引数はフレーバです。ドロップ処理は煩雑なので,別メソッドにしました。

canImportメソッドがfalseを返してきた場合,もしくは例外が発生した場合,ドロップ処理は行わず,falseを返します。

private void importString(JComponent c, String str) {
    JList list = (JList)c;
    DefaultListModel model = (DefaultListModel)list.getModel();
    int index = list.getSelectedIndex();

    // リストの選択している部分をドラッグして,
    // 選択している部分の中にドロップさせようとしたときは
    // ドロップを失敗させる
    if (indices != null
        && index >= indices[0] - 1
        && index <= indices[indices.length - 1]) {
        indices = null;
        return;
    }

    int max = model.getSize();
    if (index < 0) {
        index = max;
    } else {
        // 選択された行の次の行にドロップする
        index++;
        if (index > max) {
            index = max;
        }
    }

    addIndex = index;
    String[] values = str.split("\n");
    addCount = values.length;
    for (int i = 0; i < values.length; i++) {
        model.add(index++, values[i]);
    }
}

同一リスト中のドラッグ&ドロップは処理が煩雑になるため,連続した項目選択範囲の中にドロップをさせるのはサポートしていません。

ここでは選択された行の次の行にドロップ処理を行っています。その際,同じリスト内でドラッグ&ドロップを行っているかもしれないので,exportDoneメソッドで使用するaddCountとaddIndexを設定しています。

後は,文字列を改行で分解し,リスト・モデルに追加しているだけです。

これでListTransferHandlerクラスができあがりました。実行してみると,ドラッグ&ドロップが可能なことを確認できるはずです(図4)。

JListのドラッグ&ドロップ JListのドラッグ&ドロップ
a) リストからドラッグ b) リストからドロップ
JListのドラッグ&ドロップ JListのドラッグ&ドロップ
c) リストへドラッグ d) リストへドロップ
図4 JListのドラッグ&ドロップ

このサンプルを操作していると,ドロップするときにちょっと意に反した動きをしませんか。例えば「項目の先頭にドロップしようとすると,最後尾にドロップされてしまう」とか「項目間にドロップさせようとしても,マウスカーソルが項目間を示すカーソルに変化しない」といった点です。

こうした動作は,不具合とまではいかなくとも,もう少しどうにかならないかなぁという感じですね。これらの問題はJava SE 6を使えば解決できます。

ちょっと間が空いてしまいますが,次回のJava SE 6完全攻略で,Java SE 6でのドラッグ&ドロップの拡張を解説していきます。

なお,今回の記事ではドラッグ&ドロップの必要最低限の部分しか解説していません。ドラッグ&ドロップやカット&ペーストについてより詳しく知りたい場合には,Swingのチュートリアル「Introduction to Drag and Drop and Data Transfer」をご覧ください。

J2SE1.2やJ2SE1.3でも,AWTの機能を用いれば,Swingでドラッグ&ドロップを実装できました。

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

ツバメ

櫻庭がいつも利用している駅のそばに今年もツバメが帰ってきました。今年は暖冬だったので,ツバメが帰ってくるのも早くなるかなぁと思っていたのですが,例年とそれほど変わりありませんでした。

今は卵を温めている真最中です。

JavaOneから帰ってきたときには,かわいいヒナが生まれていることでしょう。