No Bugs, No Life

読んだ本や、プログラミング、システム開発等のねたを中心に。文章を書く練習なので少し硬派に書くつもりだけど、どうなることやら。

JavaFX:TableViewの基本的な使い方

前回はListViewについてだったので今回は予告どおりTableViewの基本的な使い方を簡単にまとめる。
あと、CellValueFactory,CellFactory辺りも。

TableView

基本的な使い方(文字列のTableView)

MWではInstalledMaterialListなどで使用しているが、このようなTableを作成する場合。
f:id:kazyury:20130407102238p:plain

TableViewにObservableListをsetItemして内部データ構造と画面上の行がをバインドする考え方は基本的にListViewと一緒なので基本的な構造としては以下のようになる。
ただし、TableViewの場合には1行のデータに複数の列があるので、tableRecordにaddするデータは何らかの構造を持ったオブジェクト(この例だったらTableRecordのインスタンス)となる。

@FXML private TableView<TableRecord> tableView;
@FXML private TableColumn<TableRecord,String> idCol;
@FXML private TableColumn<TableRecord,String> typeCol;
@FXML private TableColumn<TableRecord,String> tagCol;
private ObservableList<TableRecord> tableRecord = FXCollections.observableArrayList();

public void initialize(URL arg0, ResourceBundle arg1) {

	for (Material m:installedMaterials) {
		// observableArrayListにTableRecordを登録
		tableRecord.add(new TableRecord(m.getMaterialId(), m.getMaterialType(), tags));
	}
	tableView.setItems(tableRecord);
}

public static class TableRecord {
	private StringProperty materialId;
	private StringProperty type;
	private StringProperty concatenatedTag;

	private TableRecord(String id, String type, String tags) {
		this.materialId = new SimpleStringProperty(id);
		this.type = new SimpleStringProperty(type);
		this.concatenatedTag = new SimpleStringProperty(tags);
	}
}

あとは、TableRecordのどの属性をどの列(TableColumn)の値に対応付けるかを、CellValueFactoryを使って関連付ける。

// PropertyValueFactoryで"materialID"を指定すると、TableRecord#materialIDProperty()が呼ばれる。
idCol.setCellValueFactory(new PropertyValueFactory<TableRecord,String>("materialId"));
typeCol.setCellValueFactory(new PropertyValueFactory<TableRecord,String>("type"));
tagCol.setCellValueFactory(new PropertyValueFactory<TableRecord,String>("concatenatedTag"));

TableRecord側では、上記のPropertyValueFactoryで文字列指定されたプロパティ名にPrpperty()を付け加えたメソッドを定義しておく。
このメソッドを定義しとかないと、FX君はTableViewに表示する値を取得できない。
内部的にこのようなメソッドを呼ぶようにした想いはあるんだろうけど、なんか出来の悪い黒魔術的アプローチだなぁ。
出来のよい黒魔術ならもう少し直感的なんだろうけど(Java界隈では常識の範疇だったらゴメンナサイ)。

public static class TableRecord {
(略)
	public StringProperty materialIdProperty(){return materialId;}
	public StringProperty typeProperty(){return type;}
	public StringProperty concatenatedTagProperty(){return concatenatedTag;}

}

編集可能なTableView(TextFieldTableCellの使用)

TableViewの特定のセルを直接編集可能にしたいなんて要件もよくありがち。
JavaFXでは、テーブルのセルを標準のセル実装(TableCell)から他のセル実装(この場合は編集可能なセル実装)に差し替えることで、実現できる。
直接入力で編集するのであれば、組み込みの実装としてTextFieldTableCellが使用できる。
上の例では、データ値の連携のためにCellValueFactoryを使用したが、今回はCellそのものを差し替えるために、CellFactoryを使用する。

memoColのセルを標準のセルではなく、TextFieldTableCellにする
// memoCol はEditableに
memoCol.setCellFactory(new Callback<TableColumn<TableRecord,String>, TableCell<TableRecord,String>>() {
	@Override
	public TableCell<TableRecord, String> call(TableColumn<TableRecord, String> arg0) {
		return new TextFieldTableCell<TableRecord, String>(new DefaultStringConverter());
	}
});

TableColumn#setCellFactory()で新たなCallbackを定義し、そのcallメソッドで使用するTableCell一味を返却する。
ここでは、TextFieldTableCellインスタンスを返却するので、セルがTextFieldとなっているセルがmemoColでは使用されることになる。
なお、その他にも、TableCell (JavaFX 2.2)によると、CheckBoxTableCell, ChoiceBoxTableCell, ComboBoxTableCell, ProgressBarTableCell辺りはJavaFX2.2ならば組み込まれているので割りと簡単に使えそう。

memoColの編集終了時の処理

上記ではあくまでも表の特定のセルを、TextFieldTableCellに変更したのみであり、変更した内容は一切ObservableListの方にも反映されていない。
セルの編集終了時の処理を実装するために、TableColumn#setOnEditCommit()を実装する。

memoCol.setOnEditCommit(new EventHandler<CellEditEvent<TableRecord, String>>() {           
	@Override
	public void handle(CellEditEvent<TableRecord, String> t) {
		((TableRecord) t.getTableView().getItems().get(t.getTablePosition().getRow())).setMemo(t.getNewValue());
	}
});

セルの編集時にはCellEditEventが投げられるのでそのイベントハンドラを実装する。ここでは、選択行のTableRecordに対してsetMemoを発行している(EnsembleのTable Cell Factoryのサンプルのまんまだけど)。

TableRecord側でのsetMemo実装は以下のようなかたち。

public void setMemo(String newValue) {
	this.memo.set(newValue);
	taggedMaterial.setMemo(newValue);
	logger.info("memo was set to "+newValue);
}
TableViewを編集可能にする

上記までで特定列を編集可能なTableCellで実装するようにしたが、TableViewそのものが編集可能になっていないと駄目らしい。

tableView.setEditable(true);

でTableViewを編集可能にすればOK

ある列に画像を表示する(TableCell継承クラスの作成)

先の例にあげたとおり、TextFieldTableCell以外にもCheckBoxTableCell, ChoiceBoxTableCell, ComboBoxTableCell, ProgressBarTableCellなどは組み込みのTableCellサブクラスとして利用可能なのだが、特定の列に画像を表示する要件もよくありがち*1
先の例でTextFieldTableCell部分を、ImageViewを組み込んだ自作のTableCellにすれば実現できる。

// imageCol は画像を表示できるように
imageCol.setCellFactory(new Callback<TableColumn<TableRecord,String>, TableCell<TableRecord,String>>() {
	@Override
	public TableCell<TableRecord, String> call(TableColumn<TableRecord, String> arg0) {
		return new ImageColCell();
	}
});

ImageColCellの実装はこのようなイメージ。TableCellを継承したセルクラスを作成し、updateItemをオーバーライドすれば良さそう。
TableCellクラスの中ではsetTextで表示する文字を設定し、setGraphicで任意のNodeを配置できるっぽい。

/**
 * ImageCol用のCell
 * @author kazyury
 */
public static class ImageColCell extends TableCell<TableRecord, String> {
	private final ImageView imageView;
	
	public ImageColCell(){
		imageView = new ImageView();
		setText(null);
		setGraphic(imageView);
	}
	
	@Override
	protected void updateItem(String item, boolean empty) {
		super.updateItem(item, empty);
		if(!empty){
			FileInputStream is = null;
			try {
				is = new FileInputStream(item);
				imageView.setImage(new Image(is));
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			setText(null);
			setGraphic(imageView);
		}
	}
}

これらを組み込んだTableViewはこんなイメージになる。
f:id:kazyury:20130408224434p:plain

今回のソースはこちら(GitHub)

*1:本当か?