JTableをJavaFXで使う

JavaFXの強みのひとつとして、Swingのコンポーネントを再利用することができるということがあります。したがって、テーブルなどの機能に関しては無理にFXで書かずともSwingを使えばいいわけです。
とはいえ、簡単に使えることはいいことなので、JavaFXでJTableのラッパークラスを作ってみました。機能はまだ少ないですが、こんな感じに記述します。

   Table {
        width: 200, height: 200
        columns: [
            TableColumn {
                name: "name"
                value: function(item):Object {
                    return (item as Item).getName();
                }
            }
            TableColumn {
                name: "price"
                value: function(item):Object {
                    return (item as Item).getPrice();
                }
            }
        ]
        items: [
            new Item("商品1", 200),
            new Item("商品2", 140),
            new Item("商品3", 310)
        ]
    }


表示サンプルはこちらから。
ソースコードKenaiで公開しています。

FXでの記述

単純にJTableを使うだけでは面白くも何ともないので、JavaFXスクリプトとして綺麗にシンプルに解りやすく書けるかという所に着目します。
サンプルを見れば解るとおり、Tableというノードの属性として、columnsとitemsを定義。
itemsは汎用的なオブジェクトなので、JavaFXのクラスでもJavaのクラスでも構いません。ここではJavaのItemクラスを定義してシーケンスで定義します。
カラムの定義は、TableColumn クラスのシーケンスで定義します。何列目にどんなデータが表示されるかを定義するわけです。TableColumn にはvalueというfunctionを定義でき、ここでレンダリングする情報に変換する関数を定義しています。functionに渡される引数は各行のオブジェクトですから、キャストして必要な項目を返すだけです。
非常にシンプルと思いますが、いかがでしょうか?後はitemsの部分をサーバから取得するようにすれば、簡単にテーブルを書くことができます。

実装

実装は少し泥臭いですが、そこまで難しいものではありません。どちらかというと、上記のようなデータをどのようにJTableに展開すれば良いかを知っていれば、簡単に読むことができます。ポイントを幾つか。

Tableはjavafx.ext.swing.SwingComponentのサブクラスです。SwingのコンポーネントJavaFXで使うにはSwingComponentのサブクラスを作成し、createJComponentをオーバーライドするか、簡易ラッパー関数を使用します。ラッパー関数を使う場合、どうしても宣言的に書くことができないため、SwingComponentのサブクラスを作る方が良いでしょう。

public class Table extends SwingComponent {
    var model:TableModel = new TableModel();
    public var items: Object[] on replace {
        for (item in items) {
            model.add(item);
        }
        table.setModel(model);
    };
    var colModel:TableColumnModel = new TableColumnModel();
    public var columns: TableColumn[] on replace {
        for (column in columns) {
            colModel.addColumn(indexof column, column.name);
            model.addColumn(column);
        }
        table.setColumnModel(colModel);
    };
    var table:JTable;
    override function createJComponent(): JComponent {
        table = new JTable(model, colModel);
        return table;
    }
}

Tableでは適切なTableModelとColumnModelを設定したJTableを返すような実装を行います。
尚、このクラスはどんな実装になっているのか公開されていないので解らないのですが、initや初期パラメータの代入よりも先に、createJComponentが呼ばれるようなので注意してください。

TableColumnはJavaFXのクラスです。
ここででは、JavaFX側のクラスからJavaのコードは呼び出せないのでちょっとした工夫をしています。

public class TableColumn extends TableColumnBridge {
    public-init var name: String;
    public-init var value: function(item:Object): Object;
    override function itemToValue(item):Object {
        return value(item);
    }
}

TableColumnBridge は(名前がうまく思いつかなかったorz)Javaインターフェイスです。

public interface TableColumnBridge {
    Object itemToValue(Object item);
}

TableColumnはJavaFXのクラスなので、インターフェイスでもextendsを使って継承させます*1インターフェイスのメソッドの実装はJavaFXの文法に従ってoverride しています。
ここでのポイントはvalueという関数アトリビュートインスタンス生成時に指定し、それをitemToValueで使っている点です。
インスタンス生成時のコードを再度見ましょう。

            TableColumn {
                name: "price"
                value: function(item):Object {
                    return (item as Item).getPrice();
                }
            }

なぜ、このような事をしているかというと、Javaインターフェイスを実装しておけば、JavaFX側からJavaインスタンスを渡すことが出来るからです。

        for (column in columns) {
            colModel.addColumn(indexof column, column.name);
            model.addColumn(column); // ここから・・・
        }
public class TableModel extends AbstractTableModel {
    private List<TableColumnBridge> columns = Collections.synchronizedList(new ArrayList<TableColumnBridge>());
    private List<Object> list = Collections.synchronizedList(new ArrayList<Object>());
    public void addColumn(TableColumnBridge column) { // このメソッドに渡せる!
        columns.add(column);
    }
    // 中略
    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        return this.columns.get(columnIndex).itemToValue(this.list.get(rowIndex));
    }
}

すると、TableColumnBridge columnは本当はJavaFXクラスのインスタンスなのですが、インターフェイスで公開されている範囲でJavaから呼び出すことが出来るわけです。

このようにちょっとだけ泥臭い事をやっていますが、結果として最初に示したコードのように宣言的にテーブルを作ることができるようになったわけです。このインターフェイスを使った異なる言語間のインスタンスの受け渡しは面白いですね!

*1:JavaFXインターフェイスはなく、多重継承が可能。