JavaFX アプリケーション

JavaFX 11以上、Scene Builder、Eclipse を利用して


JavaFX の ObjectTableView を利用して行列を扱う

投稿日時:

最終更新日時:

われわれの目的は TableView を利用して要素が数値や文字列である行列を扱うことである。下図のように(実行時にサイズが変更できる、2次元配列である)行列 objectMatrix を与える。要素は、後で何でも入れれるように、Object 型とする。また、この行列の上に(例えば、列番号などの)列の名前(文字列の配列 columnValueNames)を表示する。行列の左に(例えば、行の説明などの)情報(Object 型の配列 rowDescriptions とその名前 rc00)を、必要ならば表示する。更に、編集したい要素は編集可能にし、編集したくない要素は編集不可能にする。

実現したいこと
実現したいこと

具体例は、次の通りであり、これを作成する。

行列を TableView で扱う(水色で囲った部分)
TableViewの部分

水色で囲った部分が TableView で、A、B、C、D と列の名前があり、主に整数、主に非負整数、等の行の説明が表示されている。行の説明に編集不可と書かれている行は、編集不可能であるが、その他の行は(そのセルが、編集不可、NonEditable と書かれていない限り)編集可能であり、行の説明に書かれてある通り、主に整数、主に非負整数、主に正整数、主に Doble、文字列の編集が可能である。また、TableView の下にあるステータスバーに、現時点の、(非負、正)整数の総和と Double の総和が表示されている。注意点として、どの列も複数の型のデータを扱っていることである。非負整数、正整数を扱う際に、前回登場した NonnegativeInteger、PositiveInteger を利用する。

このように動作する TableView の派生クラスである ObjectTableView とそれを利用するアプリケーション HandlingMatrixByTableView を作成する。(Eclipse のプロジェクト src の zip ファイルはこちら。)ObjectTableView を利用するアプリケーション側では objectMatrix、rowDescriptions、columnNames、rc00 を保持し、行列のサイズが変更された場合は、それを ObjectTableView に指示し、セルの内容が変更されれば、ステータスバーの総和を修正する。JavaFX の TableView に固有のテーブルの扱い方で具体的にどのように処理するかに関しては、ObjectTableView に任せる。

さて、次の図のように、JavaFX の TableView<RowType> は各行が RowType 型である、行の集まりである。行の各要素は RowType の各プロパティである。また、各列をそのプロパティの型の集まり TableColumn<RowType, PropertyType> として扱う。

TableView<RowType> (行 RowType の集まりとしての TableView)
RowTypesの集まり
TableView<RowType> (各列は TableColumn<RowType, PropertyType>、すなわち、PropertyType の集まり)
TableColumn

以上より、JavaFX の TableView では、基本的に、列数は固定されており、行の追加や削除が想定されている。また、この TableColumn が、実際の、表示(編集可能な場合は)編集を行う。

以上の想定により、実際の基本的な作業は、(1)tableView に表示する行数分の RowType のデータからなる ObservableList(data とする)を作成し、それを TableView にセットする。(2)列の個数だけ TableColumn を作成し、それに RowType のどのプロパティに対応させるかを設定する。(3)必要ならば、その列を表示(編集可能な場合は)編集する方法を指定する。(4)これらの TableColumn を tableView に加える。加えられた順に左から表示される。このようにすると、(編集が可能な場合)ユーザーの変更に応じて data が更新され、更に data.setAll(/*新しく表示したい RowType の集まり*/) と data に(クリア後)新しいデータを加えると、TableView の内容が変わる。

模擬コードで説明する。行を表す RowType は2個のプロパティ anInteger(Integer 型)と anObject(Object 型)を持つ。

public class RowType {
	private IntegerProperty anInteger = new SimpleIntegerProperty();
	private ObjectProperty<Object> anObject = new SimpleObjectProperty<Object>();
	// ...
	
	public IntegerProperty anIntegerProperty() {
		return anInteger;
	}
	
	public ObjectProperty<Object> anObjectProperty() {
		return anObject;
	}
	
	// ...
}

この RowType を複数行持つ TableView である ATableView を次のように定義する。

public class ATableView extends TableView<RowType> {
	private ObservableList<RowType> data = null;
	private TableColumn<RowType, Integer> columnInteger = null;
	private TableColumn<RowType, Object> columnObject = null;
	private Callback<TableColumn<RowType, Integer>, TableCell<RowType, Integer>> columnIntegerFactory = null;
	private Callback<TableColumn<RowType, Object>, TableCell<RowType, Object>> columnObjectFactory = null;

	public ATableView() {
		// (1) この aTableView に表示したい個数の行 RowType のインスタンスを作り、data に(クリア後)加え、それを aTableView にセットする。
		data = FXCollections.observableArrayList();
		data.addAll(new RowType(/* */), /*, as many rows as you want , */ new RowType(/* */));
		this.setItems(data);
		
		// (2-1) RowType のプロパティに対応する、表示(編集)用の TableColumn、columnInteger と columnObject を作成する。注意点は、このプロパティの型を 2 つ目に指定していることである。
		columnInteger = new TableColumn<RowType, Integer>();
		columnObject = new TableColumn<RowType, Object>();
		
		// (2-2) 今作成した columnInteger と columnObject に RowType の anInteger プロパティと anObject プロパティに対応付ける。
		columnInteger.setCellValueFactory(new Callback<CellDataFeatures<RowType, Integer>, ObservableValue<Integer>() {
			
			public ObservableValue<Integer> call(CellDataFeatures<RowType, Integer> param) {
				return param.getValue().anIntegerProperty();
			}
		});
		columnInteger.setCellValueFactory(new Callback<CellDataFeatures<RowType, Object>, ObservableValue<Object>() {
			
			public ObservableValue<Integer> call(CellDataFeatures<RowType, Object> param) {
				return param.getValue().anObjectProperty();
			}
		});

		// (3) columnInteger、columnObject の表示方法を制御する、編集可能にする、場合は、cellFactory を設定する。表示方法を制御せず、かつ、編集しない場合は不要である。		
		columnIntegerFactory = new Callback<TableColumn<RowType, Integer>, TableCell<RowType, Integer>>() {

			@Override
			public TableCell<RowType, Integer> call(TableColumn<RowType, Integer> p) {
				return new IntegerCell();
			}

		};
		columnObjectFactory = new Callback<TableColumn<RowType, Object>, TableCell<RowType, Object>>() {

			@Override
			public TableCell<RowType, Object> call(TableColumn<RowType, Object> p) {
				return new ObjectCell();
			}

		};
		columnInteger.setCellFactory(columnIntegerFactory);
		columnObject.setCellFactory(columnObjectFactory);
		
		// (4)
		this.getColumns().addAll(columnInteger, columnObject);
	}

	private class IntegerCell extends TableCell<RowType, Integer> {
		// ...
	}
	private class ObjectCell extends TableCell<RowType, Object> {
		// ...
	}
}

以上が、TableView の基本的な利用方法である。TableColumn がその列のセルを表示編集するが、例えば、上述の模擬例での columnInteger の場合は、セルは Integer 型の要素を保持していると想定されており、表示編集する時も、Integer 型のデータを扱うことを前提としている。

TableColumn にその列の名前を表示させる機能があるので、われわれの目標のイメージの rowDescriptions と objectMatrix を加えた部分が、JavaFX の TableView<RowType> の RowType を集めたものに対応する。われわれの目標とする行列を扱う TableView の列の型を前もって決めることが出来ないので、Object 型としておき、実行時にチェックし、実際の型に応じた処理を行うことにする。従って、RowType の代わりに、もう少し意味のある名前を付けて、ObjectRowVector とする。ObjectRowVector を(その時点に表示する行数分だけ)集めたものが、ObjectRowVectors である。

ObjectRowVectors と objectMatrix(rowDescriptions) の関係
ObjectRowVector と rowDescription、objectValues の関係
ObjectRowVectors

われわれに興味があるのは、行列と行の説明なので、ObjectRowVectors であり、この ObjectRowVectors は 左の rowDescriptsions、右の objectMatrix(oM) からなる。(JavaFX の TableView の扱い方のために)ObjectRowVectors を行に分けたものが ObjectRowVector であり、その一番左の要素(プロパティ) が rowDescription、それよりも右側が objectValues である。今までに登場した主なデータに関して、それらの利用場所などを下記の表にまとめた。

場所扱うデータ
アプリケーションobjectMatrix、rowDescriptions、columnNames、rc00
ObjectTableViewコンストラクタの引数として、上記のデータを得て、objectRowVectors を作成。
その過程で objectRowVector を利用。
内部で objectRowVector を利用。

ObjectRowVector の主な部分は以下の通りである。プロパティの宣言、コンストラクタ、セッター、ゲッター、である。

public class ObjectRowVector {

	private ObjectProperty<Object> rowDescription = new SimpleObjectProperty<Object>();
	private ObjectProperty<Object>[] objectValues;
	
	public ObjectRowVector(Object rowDescription, Object[] data) {
		this.rowDescription.set(rowDescription);
		this.objectValues = new ObjectProperty[data.length];
		for (int j=0; j < data.length; j++) {
			objectValues[j] = new SimpleObjectProperty<Object>();
			objectValues[j].set(data[j]);
		}
	}
	
	public ObjectProperty<Object> rowDescriptionProperty() {
		return rowDescription;
	}
	
	public void setRowDescription(Object object) {
		rowDescription.set(object);
	}
	
	public Object getRowDescription() {
		return rowDescription.get();
	}
	
	public ObjectProperty<Object> objectValuesProperty(int j) {
		return objectValues[j];
	}
	
	public void setObjectValue(int j, Object obj) {
		objectValues[j].set(obj);
	}
	
	public Object getObjectValue(int j) {
		return objectValues[j].get();
	}

	// ...	
}

ObjectTableView の概要を説明する。上で作成した ObjectRowVector を行とする、TableView の派生クラスである。コンストラクタでは、その引数を後で利用するために、private インスタンス変数に保存し、init() を呼び出す。特に、objectMatrix は、初期値はもちろんのこと、TableView 上で変更された場合も、その時点での最新の値を保持することが目的である。

public class ObjectTableView extends TableView<ObjectRowVector> {
	// ...

	public ObjectTableView(ObjectMatrix objectMatrix, boolean showColumnRowDescription, double prefCellWidth, int addedHeight, boolean isEditable, Object[] rowDescriptions, String[] columnValueNames, String rc00) {
		this.objectMatrix = objectMatrix;
		this.showRowDescription = showColumnRowDescription;
		this.prefCellWidth = prefCellWidth;
		this.addedHeight = addedHeight;
		this.isEditable = isEditable;
		this.rowDescriptions = rowDescriptions;
		this.columnValueNames = columnValueNames;
		this.rc00 = rc00;
		
		init();
		
	}
	
	// ...
}

init() では次のように、clearAndAddAllDataForTableViewFromMatrix()(後述する)は現時点の objectMatrix 等からのデータを data に加える。rowDescription(行の説明)を表示する場合は、それ用の TableColumn columnRowDescription と (この列の表示を制御する)cellFactory columnRowDescriptionFactory を作成する。RowDescriptionCell に関しては後述する。columnValues(行列)の各列に同じ (列の表示と編集を行う)cellFactory を利用するので、それ、columnValueFactory を作成する。ObjectValueCell に関しては後述する

private void init() {
	// 上記の模擬コードの(1)に対応
	data  = FXCollections.observableArrayList();
	
	// clear and add all data for tableView from objectMatrix and rowDescription
	clearAndAddAllDataForTableViewFromMatrix();
	
	this.setItems(data)
	
	// 上記の模擬コードの(2)の一部と(3)に対応
	if (showRowDescription) {
		columnRowDescription = new TableColumn<ObjectRowVector, Object>();

		// instantiate a cellFactory for RowDescription
		columnRowDescriptionFactory = new Callback<TableColumn<ObjectRowVector, Object>, TableCell<ObjectRowVector, Object>>() {

			@Override
			public TableCell<ObjectRowVector, Object> call(TableColumn<ObjectRowVector, Object>param) {
				return new RowDescriptionCell();
			}

		};
	}
	
	// instantiate a cellFactory for every column of columnValues
	columnValueFactory = new Callback<TableColumn<ObjectRowVector, Object>, TableCell<ObjectRowVector, Object>>() {

		@Override
		public TableCell<ObjectRowVector, Object> call(TableColumn<ObjectRowVector, Object> p) {
			return new ObjectValueCell();
		}

	};
	
	// 上記の模擬コードの(2)の残りと(4)に対応
	if (showRowDescription) {
		// set cellFactory and cellValueFactory for columnRowDescription, and add this columnRowDescription to this tableView
		setColumnRowDescription();
	}
	// set column name, cellValueFactory, cellFactory, and setOnEditCommit for columnValues, and add these columnValues to this tableView
	setColumnValues();

	// 残りの処理
	// cell can be selected, but row not
	this.getSelectionModel().setCellSelectionEnabled(true);
	// set tableView to be editable or not
	this.setEditable(isEditable);
	// set cell height and table height automatically
	setCellHeightAutomatically();
		
	// initially select (0, 0) cell
	if (objectMatrix != null) setSelectedCell(0, 0);
	
}

setColumnRowDescription() や setColumnValues()(後述する)は(列)TableColumn、columnRowDescription と columnValues がどのプロパティに対応するかを指定し、既に作成した cellFactory をセットする。

残りの処理として、個々のセルは選択可能であるが行は選択不可能にする。テーブル全体を編集可能(または、不可能)に設定する。セルの高さをセットし、テーブルの高さを自動的に調整する(setCellHeightAutomatically() に関しては後述する)。既定として、行列の左上のセルを選択する(setSelectedCell() に関しては後述する)。

clearAndAddAllDataForTableViewFromMatrix() では、次のように、現時点の行列 objectMatrix と rowDescriptions から、ObjectRowVectors を介して(この部分は、ObjectRowVectors のソースコードを参照して下さい)、ObjectRowVector の配列を生成し、data に(クリア後)加えている。

private void clearAndAddAllDataForTableViewFromMatrix() {
	if (objectMatrix != null) {
		ObjectRowVectors objectRowVectors = (rowDescriptions == null ? new ObjectRowVectors(objectMatrix) : new ObjectRowVectors(rowDescriptions, objectMatrix));
		data.setAll(objectRowVectors.getObjectRowVectors());
	}
	
}

RowDescriptionCell は行の説明列(rowDescriptions)のセルの表示を担当する。行いたいことは、背景色を lightgray に設定し、newItem を調べ、数値は右寄せ、左寄せや右寄せが指定された NonEditable はそのように配置し、それ以外は中央揃えで表示する。NonEditable は Object 型の value と 表示位置を表す(’l’、'c'、'r')pos を要素にもつクラスで、編集不可であることをセルの型で判断できるように作成したもので、主に文字列を保持する(詳しくは、ソースコードを参照して下さい)。アプリケーション側の利用法に関しては後述する。背景色と表示位置のみを(編集機能は不要)制御したいので、updateItem(newItem, empty) だけを次のように記述すればよい。これ以外を記述していないので、例え、この cellFactory が制御しているセルを(例えば、ダブルクリックして)編集しようとしても、何も起こらない。

private class RowDescriptionCell extends TableCell<ObjectRowVector, Object> {

	public void updateItem(Object newItem, boolean empty) {
		super.updateItem(newItem, empty);
		if (empty) {
			setText(null);
			setGraphic(null);
		} else {
			if (newItem instanceof Integer || newItem instanceof NonnegativeInteger || newItem instanceof PositiveInteger) {
				setAlignment(Pos.CENTER_RIGHT);
			} else if (newItem instanceof NonEditable) {
				if (((NonEditable)newItem).getPos() == 'r') {
					setAlignment(Pos.CENTER_RIGHT);
				} else if (((NonEditable)newItem).getPos() == 'c') {
					setAlignment(Pos.CENTER);
				}
				
			} else {
				setAlignment(Pos.CENTER);
			}
			setStyle("-fx-background-color: lightgray;");
			setText(newItem.toString());
			setGraphic(null);
		}
	}
}

ObjectValueCell は行列 objectMatrix のすべての列のセルを表示編集する。表示する部分 updateItem は RowDescriptionCell のそれと同様なので省略する。編集する部分は、セル objectItem = getItem() が NonEditable ならば編集しない、Integer、NonnegativeInteger、PositiveInteger、Double ならば、前回作成した NumberField を、String ならば、TextField を利用して編集させる。

ObjectValueCell のコードは次のようである。編集機能を利用する場合は、startEdit()、cancelEdit() を記述する。startEdit() では、セル objectItem = getItem() が NonEditable ならば、キャンセルする(cancelEdit() を呼ぶ)。セルが String ならば、編集用に TextField を利用するために、startEditForString() を呼ぶ。セルが Integer、NonnegativeInteger、PositiveInteger、Double ならば、それを指すように type を指定して、startEditForNumber(type) を呼ぶ。startEditForNumber(type) では type に応じて適切な numberField を生成し、それに現在のセルの値をセットする。numberField にイベントリスナーとイベントハンドラーを記述し、テキストを空にし、numberField をグラフィックにセットし、numberField の内容を選択し、キーボードフォーカスを要求する。イベントリスナーとイベントハンドラーの部分は、(タブキーを押し)フォーカスを失ったら、または、(エンターキーを押し)アクションイベントが起動したら、編集が確定した処理(commitEdit(numberField.getValue()) を呼ぶ)をし、キャンセルキーが押されたら、キャンセルする(cancelEdit() を呼ぶ)。onActionCalled の部分はエンターキーを押したときに、直後に呼ばれる changeListener でもう一度 commitEdit が呼ばれないようにするためである。

private class ObjectValueCell extends TableCell<ObjectRowVector, Object> {
	private NumberField numberField;
	private TextField textField;
	private boolean onActionCalled = false;
	
	private void startEditForString() {
		// ... 省略
	}
	
	private void startEditForNumber(int type) {
		numberField = new NumberField(type);
		numberField.setValue(getItem());
		numberField.focusedProperty().addListener(new ChangeListener<Boolean>() {

			@Override
			public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue,
					Boolean newValue) {
				if (!newValue) {
					if (onActionCalled) {
						onActionCalled = false;
						return;
					}
					commitEdit(numberField.getValue());
				}
			}

		});

		numberField.setOnAction(new EventHandler<ActionEvent>() {

			@Override
			public void handle(ActionEvent event) {
				onActionCalled = true;
				commitEdit(numberField.getValue());
			}

		});
		
		numberField.setOnKeyPressed(new EventHandler<KeyEvent>() {

			@Override
			public void handle(KeyEvent event) {
				if (event.getCode() == KeyCode.ESCAPE) {
					cancelEdit();
				}
			}
			
		});

		setText(null);
		setGraphic(numberField);
		numberField.selectAll();
		numberField.requestFocus();
	}

	public void startEdit() {
		if (!isEmpty()) {
			super.startEdit();
			Object objectItem = getItem();
			
			if (objectItem instanceof NonEditable) {
				cancelEdit();
				return;
			}
			
			if (objectItem instanceof String) { // String
				startEditForString();
				return;
			}
			
			// Integer, NonnegativeInteger, PositiveInteger, Double
			int type = NumberField.INTEGER;
			if (objectItem instanceof Integer) {
				type = NumberField.INTEGER;
			}
			if (objectItem instanceof NonnegativeInteger) {
				type = NumberField.NONNEGATIVE_INTEGER;
			}
			if (objectItem instanceof PositiveInteger) {
				type = NumberField.POSITIVE_INTEGER;
			}
			if (objectItem instanceof Double) {
				type = NumberField.DOUBLE;
			}
			startEditForNumber(type);
			return;
		}

	}

	public void cancelEdit() {
		super.cancelEdit();
		Object object = getItem();
		String str = object.toString();
		setText(str);
		setGraphic(null);
	}

	public void updateItem(Object newItem, boolean empty) {
		// ... 省略
	}
	
}

キャンセルされた時に呼ばれる cancelEdit() では、セルの内容の文字列表現をテキストに設定し、グラフィックを空にする。

setColumnValues() では、objectMatrix の列数の個数分の TableColumn を作成し、それらの配列として、columnValues を定義する。columnValues に列名を設定し(setText())、各列に objectMatrix のどの列を対応させるかを設定し(setCellValueFactory())、各列の表示編集方法として各列共通の columnValueFactory を設定し(setCellFactory())、最後に、この TableView に columnValues を加え、列を入れ替えることが出来ないように設定し、列内で並べ替えを禁止する、などである。

private void setColumnValues() {
	int numberOfColumns = (objectMatrix != null ? objectMatrix.getNumberOfColumns() : (columnValueNames != null ? columnValueNames.length : 0));
	columnValues = new TableColumn[numberOfColumns];
	for (int j = 0; j < numberOfColumns; j++) {
		final int jj = j;
		columnValues[j] = new TableColumn<ObjectRowVector, Object>();
		if (columnValueNames != null) {
			columnValues[j].setText(columnValueNames[j]);
		}
		columnValues[j].setCellValueFactory(new Callback<CellDataFeatures<ObjectRowVector, Object>, ObservableValue<Object>>() {

			@Override
			public ObservableValue<Object> call(CellDataFeatures<ObjectRowVector, Object> param) {
				return param.getValue().objectValuesProperty(jj);
			}

		});

		columnValues[j].setCellFactory(columnValueFactory);
		columnValues[j].setOnEditCommit(new EventHandler<CellEditEvent<ObjectRowVector, Object>>() {

			@Override
			public void handle(CellEditEvent<ObjectRowVector, Object> event) {
				int row = event.getTablePosition().getRow();
				Object newObject = event.getNewValue();
				((ObjectRowVector) event.getTableView().getItems().get(row)).setObjectValue(jj, newObject); // update data
				objectMatrix.setValue(newObject, row, jj); // update objectMatrix
				cellUpdated.set(cellUpdated.get()+1); // for a parent process to know update
			}
			
		});

	}

	this.getColumns().addAll(columnValues);
	for (int i = 0; i < numberOfColumns; i++) {
		// horizontal position of this columnValues[i] cannot change
		columnValues[i].setReorderable(false);
		// vertical position within this columnValues[i] cannot change
		columnValues[i].setSortable(false);
		columnValues[i].setPrefWidth(prefCellWidth);
	}
	
}

説明し残した、setOnEditCommit(・・・) に関して説明する。この中の handle() の部分が、既述の ObjectValueCell で出てきた commitEdit(・・・) で実行される。われわれの目的は、コンストラクタで初期値を保存した行列 objectMatrix を最新の状態にしておくことである。(アプリケーション側で、最新の行列を、objectTableView.getObjectMatrix() によって取得したい。)また、ユーザーの編集等により、行列 objectMatrix が変化した時点(で整数や Double の総和を、アプリケーション側で計算したいので、その時点)を、アプリケーション側で知る仕組みが欲しいのである。行列を最新状態にする必要もなく、かつ、アプリケーション側に行列が変化した時点を知らせる必要もなければ、setOnEditCommit(・・・) の部分を記述しなくても、ユーザーがセルを変更すれば、新しい値がそのセルに表示される。更に、注意点は、setOnEditCommit(・・・) を記述する場合は、(しなかった場合は、自動で行ってくれた)

int row = event.getTablePosition().getRow();
Object newObject = event.getNewValue();
((ObjectRowVector) event.getTableView().getItems().get(row)).setObjectValue(jj, newObject); // update data

の部分も記述しなければならない。独自で追加するのは、次の2行である。最初の行は、objectMatrix の対応する要素(第 row 行の第 jj 列)を新しい値 newObject にセットする。次の行が行列(のある要素)が更新されたことをアプリケーション側が受け取る時点を知る方法である。

objectMatrix.setValue(newObject, row, jj); // update objectMatrix
cellUpdated.set(cellUpdated.get()+1); // for a parent process to know update

まず、cellUpdated という IntegerProperty を宣言する。・・・Property と宣言することによって、アプリケーション側でそのチェンジリスナーを作成でき、これにより、行列(のある要素)が更新されたことを知ることが出来る。実際に値が変化しなくてはならないので、objectMatrix (のある要素)が更新されれば、1 だけ増加させる。(次の可能性は殆どないが、整数の最大値を超えそうになったら 0 にセットしなおす。)

private IntegerProperty cellUpdated = new SimpleIntegerProperty(0);

public void setCellUpdated(int j) {
	if (j >= Integer.MAX_VALUE - 1) {
		cellUpdated.set(0);
	} else {
		cellUpdated.set(j);
	}
}

public int getCellUpdated() {
	return cellUpdated.get();
}

public IntegerProperty cellUpdatedProperty() {
	return cellUpdated;
}

setCellHeightAutomatically() は次のように、行の高さを24に設定し、テーブルの希望の高さを、(今設定した値)*(行数)+(addedHeight)に設定している。なお、この部分はここを参照した。(addedHeight を導入したのは、状況によりスクロールバーが表示されることに対応したものです。)

private void setCellHeightAutomatically() {
	this.setFixedCellSize(24);
	this.prefHeightProperty().bind(Bindings.size(this.getItems()).multiply(this.getFixedCellSize()).add(addedHeight));
}
行列の修正に対応する TableView の更新

さて、アプリケーション側の理由で(行数や列数を変更することにより) objectMatrix を変更したことに対応することが望ましい。その部分が、次の、updateTabe(newObjectMatrix, rowDiscriptions, columnValueNames) である。概要は、現在選択されている行と列(選択されているセルの位置)を保存し、現在の列を取り除き、新しい行列 newObjectMatrix を objectMatrix に代入し(必要ならば、他の引数も)、tableView に新しいデータを表示するために data を更新し(clearAndAddAllDataForTableViewFromMatrix())、新しい列をセットし、最初に保存した位置のセルを選択する。

public void updateTable(ObjectMatrix newObjectMatrix, Object[] rowDescriptions, String[] columnValueNames) {
	// find a selected cell index before update
	int rowIndex = getSelectedRowIndex();
	int columnIndex = getSelectedColumnIndex();
	
	// remove old columns, rowDescriptions and columnValues.
	this.getColumns().clear();
	
	if (rowDescriptions != null) this.rowDescriptions = rowDescriptions;
	if (columnValueNames != null) this.columnValueNames = columnValueNames;
	this.objectMatrix = newObjectMatrix;

	// clear and add all data for tableView from objectMatrix and rowDescription
	clearAndAddAllDataForTableViewFromMatrix();

	// set updated new columns, rowDescriptions and columnValues
	if (showRowDescription) {
		setColumnRowDescription();
	}
	setColumnValues();
	
	// select cell (rowIndex, columnIndex) in updated table
	setSelectedCell(rowIndex, columnIndex);
}

上記で、getSelectedRowIndex() と getSelectedColumnIndex()(とそれに関連する getSelectedCell())と setSelectedCell(rowIndex, columnIndex) は、次のようである。

/**
 * row index of selected cell
 * -1 if not selected
 * @return
 */
public int getSelectedRowIndex() {
	int rowIndex = -1;
	TablePosition cell = getSelectedCell();
	if (cell == null) return rowIndex;
	rowIndex = cell.getRow();
	return rowIndex;
}

/**
 * column index of selected cell
 * -1 if not selected
 * @return
 */
public int getSelectedColumnIndex() {
	int columnIndex = -1;
	TablePosition cell = getSelectedCell();
	if (cell == null) return columnIndex;
	TableColumn column = null;
	column = cell.getTableColumn();
	for (int j=0; j < objectMatrix.getNumberOfColumns(); j++) {
		if (column == columnValues[j]) {
			columnIndex = j;
			break;
		}
	}
	return columnIndex;
}

/**
 * find selected cell
 * @return
 */
private TablePosition getSelectedCell() {
	TableViewSelectionModel<ObjectRowVector> model = getSelectionModel();
	ObservableList<TablePosition> cells = model.getSelectedCells();
	TablePosition cell = null;
	if (!cells.isEmpty()) {
		cell = cells.get(0);
	}
	return cell;
}

setSelectedCell(rowIndex, columnIndex) は以下のようである。サイズが小さな行列に更新された場合は、なるべく近いセルを選択する。

/**
 * set (rowIndex, columnIndex) or nothing to be selected if (rowIndex != -1 || columnIndex != -1) or not
 * @param rowIndex
 * @param columnIndex
 */
public void setSelectedCell(int rowIndex, int columnIndex) {
	TableViewSelectionModel<ObjectRowVector> model = getSelectionModel();
	if (rowIndex == -1 || columnIndex == -1) {
		model.clearSelection();
	} else {
		rowIndex = Math.min(rowIndex, objectMatrix.getNumberOfRows()-1);
		columnIndex = Math.min(columnIndex, objectMatrix.getNumberOfColumns()-1);
		model.clearAndSelect(rowIndex, columnValues[columnIndex]);
	}
}

以上で、ObjectTableView の説明は終わった。次に、これを利用したアプリケーション HandlingMatrixByTableView の説明に入る。

行列を TableView で扱う
HandlingMatrixByTableView

Scene Builder での作業は次の通りである。

Scene Builder での作業 HandlingMatrixByTableViewMainVBox
(VBox の派生クラス)変数名
Scene Builder

Main クラスは次の通りである。注意点は main(args) で引数の有無を調べ、あれば showRowDescriptions を false に設定し、Scene Builder で作成した VBox の派生クラス(コントローラーでもある)である HandlingMatrixByTableViewMainVBox のコンストラクタに showRowDescriptions を引数として渡している所である。

public class Main extends Application {
	
	static private boolean showRowDescriptions = true;
	@Override
	public void start(Stage primaryStage) {
		try {
			HandlingMatrixByTableViewMainVBox root = new HandlingMatrixByTableViewMainVBox(showRowDescriptions);
			Scene scene = new Scene(root, 360, 310);
			primaryStage.setTitle("行列を TableView で扱う");
			primaryStage.setScene(scene);
			primaryStage.show();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		if (args.length > 0) showRowDescriptions = false;
		launch(args);
	}
}

次に、HandlingMatrixByTableViewMainVBox の説明に移る。次のようなインスタンス変数を定義する。

private String[] comboBoxData = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};

private int numberOfRows = 6;
private int numberOfColumns = 4;

private ObjectMatrix objectMatrix = null;
private Object[][] defaultObjectMatrixData = { // 10 X 10 matrix
		{Integer.valueOf(-1), Integer.valueOf(0), Integer.valueOf(1), new NonEditable("編集不可"), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)},
		{NonnegativeInteger.valueOf(0), NonnegativeInteger.valueOf(1), new NonEditable("NonEditable"), NonnegativeInteger.valueOf(3), NonnegativeInteger.valueOf(0), NonnegativeInteger.valueOf(0), NonnegativeInteger.valueOf(0), NonnegativeInteger.valueOf(0), NonnegativeInteger.valueOf(0), NonnegativeInteger.valueOf(0)},
		{PositiveInteger.valueOf(1), Double.valueOf(2.0), PositiveInteger.valueOf(3), PositiveInteger.valueOf(4), PositiveInteger.valueOf(1), PositiveInteger.valueOf(1), PositiveInteger.valueOf(1), PositiveInteger.valueOf(1), PositiveInteger.valueOf(1), PositiveInteger.valueOf(1)},
		{Integer.valueOf(-1), Double.valueOf(0.5), Double.valueOf(Math.E), Double.valueOf(Math.PI), Double.valueOf(0), Double.valueOf(0), Double.valueOf(0), Double.valueOf(0), Double.valueOf(0), Double.valueOf(0)},
		{"私は", "JavaFXの", "TableView で表示した", "行列です", "", "", "", "", "", ""},
		{new NonEditable("0.5"), new NonEditable("1/2"), new NonEditable("半分"), new NonEditable("half"), new NonEditable("0.25"), new NonEditable("1/4"), new NonEditable("4分の1"), new NonEditable("quarter"), new NonEditable("不変"), new NonEditable("immutable")},
		{"", "", "", "", "", "", "", "", "", ""},
		{"", "", "", "", "", "", "", "", "", ""},
		{"", "", "", "", "", "", "", "", "", ""},
		{"", "", "", "", "", "", "", "", "", ""}
		};
private ObjectTableView objectTableView = null;
private String rc00 = null;
private Object[] defaultRowDescriptions = {"主に整数", new NonEditable("主に非負整数"), new NonEditable("主に正整数", 'l'), new NonEditable("主に Double", 'r'), new NonEditable("文字列"), new NonEditable("編集不可"), "文字列", "文字列", "文字列", "文字列"};
private String[] valueNames = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"};

private boolean showRowDescriptions = true;

defaultObjectMatrixData は 10 行 10 列の Object 型の配列で、HandlingMatrixByTableView で扱う最大サイズの行列のデータを定義している。注意点は、各要素は、Integer、NonnegativeInteger、PositiveInteger、Double、NonEditable、String のどれかであり、どの型か(初期値も含め)を明確に定義している。defaultRowDescriptions も(最大サイズの)10 列の Object 型の配列で、要素は String、NonEditable である。例えば、new NonEditable("主に正整数", 'l') は編集不可能な文字列 "主に正整数" であり、左寄せで表示される。

コンストラクタでは、引数を、後で利用するために、インスタンス変数に代入している。

public HandlingMatrixByTableViewMainVBox(boolean showRowDescriptions) {
	this.showRowDescriptions = showRowDescriptions;
	
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("HandlingMatrixByTableViewMainVBox.fxml"));
	fxmlLoader.setRoot(this);
	fxmlLoader.setController(this);
       	
	try {
		fxmlLoader.load();
	} catch (IOException exception) {
		throw new RuntimeException(exception);
	}

}

FXMLLoader の初期化が完了した時に呼ばれる initialize() は次の通りである。コンボボックスを初期化し、赤色で引数に関する説明を表示する。setTable() で ObjectTableView をセットし、この時点での整数と Double の総和を求め表示し、イベントハンドラーとイベントリスナーを設定する。

@FXML
private void initialize() {
	comboBoxNumberOfRows.getItems().setAll(comboBoxData);
	comboBoxNumberOfColumns.getItems().setAll(comboBoxData);
	comboBoxNumberOfRows.setValue(""+numberOfRows);
	comboBoxNumberOfColumns.setValue(""+numberOfColumns);
	labelShowRowDescriptions.setStyle("-fx-text-fill: red;");
	if (showRowDescriptions) {
		labelShowRowDescriptions.setText("引数を付けて起動すると、行の説明が表示されません。");
	} else {
		labelShowRowDescriptions.setText("引数を付けずに起動すると、行の説明が表示されます。");
	}
	
	setTable();
	computeSum();
	
	setHandlers();
	setListeners();
	
}

setTable() は次の通りである(setTable() は後述のイベントハンドラ内からも呼び出される)。defaultObjectMatrixData の中で、呼び出される時点での numberOfRows 行、numberOfColumns 列の部分を newObjectMatrixData に取り出す(getDefaultObjectMatrixData())。もし既に、objectMatrix が存在していたら、 newObjetMatrixData に復元する。この newObjectMatrixData から objectMatrix を作る。もし、objectTableView が存在していなければ、新規に作成し(この TableView を表示するために、Scene Builder 上で作成しておいた VBox) vBoxTableView に加える。もし、objectTableView が存在していれば、新しいデータ objectMatix 等で更新する(objectTableView.updateTable(・・・))。

private void setTable() {
	Object[][] oldObjectMatrixData = null;
	
	// store oldObjectMatrixData if any
	if (objectMatrix != null) {
		oldObjectMatrixData = objectTableView.getObjectMatrix().getData();
	}
	
	Object[][] newObjectMatrixData = getDefaultObjectMatrixData();
	
	// restore oldObjectMatrixData if any
	if (oldObjectMatrixData != null) {
		int iMin = Math.min(oldObjectMatrixData.length , numberOfRows);
		for (int i=0; i < iMin; i++) {
			int jMin = Math.min(oldObjectMatrixData[i].length, numberOfColumns);
			for (int j=0; j < jMin; j++) {
				newObjectMatrixData[i][j] = oldObjectMatrixData[i][j];
			}
		}
	}
	
	objectMatrix = new ObjectMatrix(newObjectMatrixData);
	if (objectTableView == null) { // instantiate objectTableView if not exists
    	objectTableView = new ObjectTableView(objectMatrix, showRowDescriptions, 60, 54, true, getRowDescriptions(), valueNames, rc00);
    	vBoxTableView.getChildren().add(objectTableView);
	} else { // updateTable if there exists objectTableView
		objectTableView.updateTable(objectMatrix, getRowDescriptions(), valueNames);
	}
	
}

objectTableView を新規に作成する時のコンストラクタの引数の意味は、行列 objectMatrix、説明列を表示するか否か showRowDescriptions、セルの希望幅 60、テーブル表示用に付加する高さ 54、編集可能か否か true、行の説明 getRowDescriptions()、行列の列の名前 valueNames、行の説明列の名前 rc00、である。行の説明は Object 型の配列であるが、その長さが objectMatrix の行数と同じ必要がある。一方、行列の列の名前 valueNames の長さは、objectMatrix の列数以上であればよい。

computeSum() は以下のように、その時点での最新の行列の整数値(非負整数と正整数も含む)と Double の総和を求め、対応するラベル labelIntegerSum と labelDoubleSum に表示する。objectTableView の objectMatrix には最新のデータが保持されているので、それを参照し総和を求めればよい。

private void computeSum() {
	ObjectMatrix oMatrix = objectTableView.getObjectMatrix();
	int sumInt = 0;
	double sumDouble = 0.0;
	for (int i=0; i < numberOfRows; i++) {
		for (int j=0; j < numberOfColumns; j++) {
			Object object = oMatrix.getValue(i, j);
			if (object instanceof Integer) {
				sumInt += ((Integer)object).intValue();
			}
			if (object instanceof NonnegativeInteger) {
				sumInt += ((NonnegativeInteger)object).getValue().intValue();
			}
			if (object instanceof PositiveInteger) {
				sumInt += ((PositiveInteger)object).getValue().intValue();
			}
			if (object instanceof Double) {
				sumDouble += ((Double)object).doubleValue();
			}
		}
	}

	labelIntegerSum.setText(""+sumInt);
	labelDoubleSum.setText(""+sumDouble);
	
}

setHandlers() では、2つのイベントハンドラーを記述している。「サイズの変更」ボタンを押したときのハンドラーは

buttonChangeSize.setOnAction(new EventHandler<ActionEvent>() {

	@Override
	public void handle(ActionEvent event) {
		int newNumberOfRows = comboBoxNumberOfRows.getSelectionModel().getSelectedIndex() + 1;
		int newNumberOfColumns = comboBoxNumberOfColumns.getSelectionModel().getSelectedIndex() + 1;
		if (newNumberOfRows == numberOfRows && newNumberOfColumns == numberOfColumns) {
			return;
		}
		numberOfRows = newNumberOfRows;
		numberOfColumns = newNumberOfColumns;
		setTable();
		computeSum();
	}
	
});

コンボボックスから新しい行数と列数を調べ、現時点のものと(少なくとも1つ)異なれば、numberOfRows と numberOfColumns を修正し、setTable() を呼び出し、新しい行列に修正し、computeSum() で総和を計算しなおす。

チェックボックス「列幅を揃える」のハンドラーは、次の通りである。

checkBoxAlignCellWidth.setOnAction(new EventHandler<ActionEvent>() {

	@Override
	public void handle(ActionEvent event) {
		setAlignCellWidth(checkBoxAlignCellWidth.isSelected());
	}
	
});

setAlignCellWidth(align) は次の通りで、「列幅を揃える」がチェックされていれば、ウィンドウの幅に合うようにテーブルの幅が調整され、更に、テーブルの列幅を揃える。

public void setAlignCellWidth(boolean align) {
	if (align) {
		objectTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
	} else {
		objectTableView.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
	}
}

setListeners() は objectTableView のセルが変更された時のイベントリスナーを記述している。objectTableView で作成した IntegerProperty である cellUpdated が変化したら、computeSum() を呼び出し、行列の整数と Double の総和を計算しなおしている。

objectTableView.cellUpdatedProperty().addListener(new ChangeListener<Number>() {

	@Override
	public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
		computeSum();
	}
	
});

HandlingMatrixByTableView を引数を付けて起動すると、次の図のように、行の説明をしている列が表示されなくなる。

引数を付けて起動した場合
引数付き

Eclipse プロジェクト HandlingMatrixByTableView の src の zip ファイルはこちら。実行ファイルはこちら