ちょっと間が空いてしまいましたが,前回に引き続きSwingのドラッグ&ドロップについて解説しましょう。

前回はJ2SE 5.0までのSwingのドラッグ&ドロップについて説明しました。今週は,Java SE 6のドラッグ&ドロップに関する強化点を説明していきます。

ドラッグ開始操作の改良

J2SE 5.0では,JListクラスやJTreeクラスでドラッグを行うときに,一度ドラッグしたい項目をマウス・クリックで選択し,もう一度クリックをしないとドラッグ操作になりませんでした。しかし,こうしたドラッグ開始の操作は,他のネイティブなアプリケーションの動作とは異なります。通常は1度のマウス・クリックでドラッグ操作が可能です。

このため,どうしても操作に違和感が生じてしまいます。

そこで,Java SE 6では次のように改良されました。

  • JListクラス,JTreeクラス,JFileChooserクラス
    2度のマウス・クリックは必要なくなり,1度の選択でドラッグ操作に入れるようになりました。
    また,シフトキーやコントロールキーを押しながらのドラッグもスムースに行われるようになっています。
  • JTableクラス
    基本的にはJListなどの改良と同様ですが,項目の選択がSINGLE_SELECTIONのときだけに制限されます。これは,テーブルの複数の項目を選択するときにマウス・ドラッグが使われるためです。このため,複数項目選択可能なときは,1度目のドラッグで項目を選択し,次のドラッグでデータ転送を行うようになっています。

こうした改良の恩恵を受けるのに再コンパイルは必要ありません。単にJava SE 6で実行するだけで,すぐにこの改良を利用できます。

なお,この改良はJ2SE 5.0にバックポートされ,J2SE 5.0 update 5から使えるようになっています。

ただし,今までの動作と変わってしまうので,デフォルトでは使用できません。この改良を使えるようにするには次に示す起動時オプションが必要です。

>java -Dsun.swing.enableImprovedDragGesture [クラス名]

ドロップ操作の強化

前回のSwingのドラッグ&ドロップの解説の最後に,ドロップ時の挙動がおかしいと書きました。

これは,ドロップ位置を示すための表示が適切でないことに起因します。例えば,リストに要素を挿入したい場合でも,ドロップ位置の表示はリストの要素を選択した場合と同じになってしまいます。

このようなドロップ操作の挙動の違和感は,JListクラス,JTreeクラス,JTableクラスなど項目が並んでいるコンポーネントに目立ちます。

Java SE 6では,この問題を解決するために,列挙型のjavax.swing.DropModeが導入されました。

DropModeには表1に示す値が定義されています。

表1 DropMode
使用できるコンポーネント 説明
ON JList, JTree, JTable 既存の項目のインデックスに基づいてドロップ位置が決められる
INSERT JList, JTree, JTable,
JTextComponent
データを挿入することを前提にして,ドロップ位置が決められる
ON_OR_INSERT JList, JTree, JTable ONとINSERTのコンビネーション
[ON_OR_]INSERT_ROWS
[ON_OR_]INSERT_COLS
JTable JTableに特化した値であり,列もしくは行だけでドロップ位置が決められる
USE_SELECTION JList, JTree, JTable,
JTextComponent
バックワード・コンパチビリティのために設定されており,デフォルトの動作になる
J2SE 5.0までの動作と同一

表1に示したコンポーネントにはsetDropModeメソッドが新たに定義されており,DropModeを設定できます。とはいうものの,これらのモードはTransferHandlerクラスがそれらのモードを扱えることが前提になっています。

それでは,前回のJListクラス用に作成したListTransferHandlerクラスを拡張して,これらのモードを扱えるようにしてみましょう。

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

今までのTransferHandlerクラスではドロップ位置に関する情報は何もありませんでした。そのため,ドロップ先のコンポーネントで選択されている位置を基準にしてドロップをするしかありませんでした。

Java SE 6ではドロップ位置を扱うためのクラスが導入されています。TransferHandlerクラスの内部クラスであるTransferHandler.DropLocationクラスがそれにあたります。

また,今までドロップ先のコンポーネントやフレーバなどをバラバラに扱っていたのを,TransferHandlerクラスの内部クラスTransferHandler.TransferSupportクラスでまとめて扱えるようになりました。DropLocationクラスもTransferSupportクラスのフィールドとして保持しています。

TransferSupportクラスを扱うために,TransferHandlerクラスのcanImportメソッドとimportDataメソッドがオーバロードされています。

これら二つのメソッドはどのように実装すればいいのでしょうか。サンプルのコードを見てみましょう。

public boolean canImport(TransferSupport support) {
    // クリップボード経由のデータ転送は扱わない
    if (!support.isDrop()) {
        return false;
    }

    // ドロップする位置を常に表示する
    support.setShowDropLocation(true);

    // 文字列以外のフレーバは受け入れない
    if (support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
        return true;
    }

    return false;
}

赤字で示した部分がTransferSupportクラスのメソッドです。今までは,このメソッドで表される処理も自作しなくてはならず,コードが長くなっていました。それが,TransferSupportクラスで定義されたことにより,すっきりしています。

ただ,TransferSupportクラスによってcanImportメソッドのコードは簡潔になったものの,記述する処理自体はJ2SE 5.0までと変わりません。

ドロップする位置に関する処理はimportDataメソッドに記述します。なお,今回のサンプルは簡略化のため,リストのドラッグはサポートしていません。

public boolean importData(TransferSupport support) {
    if (canImport(support)) {
        try {
            // 1. ドロップする位置を決定する
            JList.DropLocation location
                = (JList.DropLocation)support.getDropLocation();

            // 2. ドロップするデータを用意する
            Object obj = support.getTransferable()
                             .getTransferData(DataFlavor.stringFlavor);
            String text = (String)obj;

            // 3. ドロップ先のコンポーネントを取得
            JList list = (JList)support.getComponent();
            DefaultListModel model = (DefaultListModel)list.getModel();

            // 4. データをドロップする
            if (location.isInsert()) {
                // 挿入が可能な場合
                model.add(location.getIndex(), text);
            } else {
                // 挿入が不可の場合,要素を置き換える
                model.setElementAt(text, location.getIndex());
            }

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

    return false;
}

コメントの1.の部分で行っているのが,ドロップする位置の決定です。TransferSupportクラスのgetDropLocationメソッドの戻り値はDropLocationオブジェクトです。しかし,このままでは実際にどこにドロップすればいいのかわかりません。

そこで,各コンポーネントではDropLocationクラスを派生させたロケーションを表すクラスを定義しています。例えば,JListクラスはJList.DropLocationクラス,JTreeクラスはJTree.DropLocationクラス,JTableクラスはJTable.DropLocationクラス,といった具合です。いずれも,内部クラスとして定義しています。

DropLocationクラスはドロップ位置をjava.awt.Pointクラスで表します。これに対し,JList.DropLocationクラスやJTree.DropLocationクラスはインデックス,JTable.DropLocationクラスは列と行で表します。

JList.DropLocationオブジェクトが得られたので,次はドロップするデータを用意し,ドロップ先のコンポーネントを取得します。これらはJ2SE 5.0まではimportDataメソッドの引数になっていましたが,Java SE 6ではTransferSupportクラスにまとめられています。

最後にデータをドロップします。

JList.DropLocationクラスでは,ドロップ位置が挿入であるかどうかをisInsertメソッドで調べられます。挿入の場合はリスト・モデルに加え,挿入でないときには置き換えることにしました。

いずれの場合もドロップの位置はJList.DropLocationクラスのgetIndexメソッドで調べられます。getIndexメソッドの戻り値はintで,リストのインデックスを表しています。

これで,ListTransferHandler2クラスはできあがりました。

せっかくですから,DropModeの違いによるドロップ動作がわかるようにコンボボックスでDropModeを選択できるようにしてみます。JListDnD2クラスのコンボボックスを生成する部分を以下に示します。

private JComboBox createComboBox() {
    map = new LinkedHashMap<String, DropMode>();
    map.put("USE_SELECTION", DropMode.USE_SELECTION);
    map.put("INSERT", DropMode.INSERT);
    map.put("ON", DropMode.ON);
    map.put("ON_OR_INSERT", DropMode.ON_OR_INSERT);

    JComboBox box = new JComboBox();
    for (String item: map.keySet()) {
        box.addItem(item);
    }

    box.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent event) {
            JComboBox box = (JComboBox)event.getSource();
            String item = (String)box.getSelectedItem();

            // ドロップモードを設定する
            DropMode mode = map.get(item);
            list.setDropMode(mode);
        }
    });

    return box;
}

ここでは,コンボボックスで表示する文字列とDropModeの値をマップに保持しています。コンボボックスの値が変更されたら,それに対応するDropModeの値をマップから取り出し,setDropModeメソッドを用いてリストにセットしています。

では,実行してみましょう。

図1はデフォルトのUSE_SELECTIONでの動作です。リストの項目を選択していたとしても,その選択は無視されてしまいます。

USE_SELECTIONでの実行結果 USE_SELECTIONでの実行結果
a) ドラッグ前 b) ドラッグ
USE_SELECTIONでの実行結果  
c) ドロップ  
図1 USE_SELECTIONでの実行結果

図2がINSERTの動作です。項目間に挿入することを明示する表示が行われます。また,ドラッグ前にリストで選択されていた項目は,ドロップ後も選択状態のままになります。

INSERTでの実行結果 INSERTでの実行結果
a) ドラッグ b) ドロップ
図2 INSERTでの実行結果

図3がONの動作です。一見,USE_SELECTIONと同じように見えますが,ドラッグ前の選択項目はそのまま選択された状態になっています。

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

図4がON_OR_INSERTの結果です。マウス・カーソルが項目間にあるときはINSERTと同じ表示になり,項目がある場所ではONと同じ表示になります。

ON_OR_INSERTでの実行結果 ON_OR_INSERTでの実行結果
a) 項目間でのドラッグ b) 項目のある部分でのドラッグ
図4 ON_OR_INSERTでの実行結果

今後は,Swingでドラッグ&ドロップを使用するときには,モードの違いを認識して,アプリケーションの動作に適切なモードを選択する必要があります。

ドラッグ&ドロップの強化点はまだあるのですが,ずいぶん記事が長くなってしまいました。そこで,来週もドラッグ&ドロップを取りあげたいと思います。

著者紹介 櫻庭祐一

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

今月の櫻庭

ツバメ

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

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

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