JavaFX でコンポーネント作って動的生成してみる

# この記事について

将来的に書く予定の「JavaFX で DynamoDB Viewer作ってみた」記事の１ステップ。
結構大きな話になると思うので、少しずつ技術ポイント毎に記事を書いて、ある一定程度の要件を満たせた段階で前述まとめ記事書く予定。

第一回記事：[DynamoDBの情報を読み込んでJavaFXで表示してみる](https://qiita.com/silverbox/items/eba824a9f003df67181e)
第二回記事：[JavaFXで動的にテーブル列を設定する](https://qiita.com/silverbox/items/904a01d561014d800696)
第三回記事：[AWS java SDKでDynamoDBテーブル情報を取得してみる](https://qiita.com/silverbox/items/3caefb782f1bb269490f)
第四回記事：[JavaFX の TableView の選択範囲をクリップボードにコピーする。](https://qiita.com/silverbox/items/19e131bd3da02940a8c1)

※これまでの記事が基本になってます。メソッドなど細かい部分で再説明していない部分があります。不明点などありましたらコメントなど頂けたら対応しようと思います。

# 今回の追加機能

今は別のテーブルを見る時、前回読んだ情報は消える。大体において何かの調査などをする時、複数のテーブルを同時に見る。読み込み結果をいくつも保持してタブ形式などで切り替えたい。

# 現在の進捗

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/f53c342d-6c4e-c523-6202-0e6c7d12afda.png)

[この時点でのソース(github)](https://github.com/silverbox/dynamodbtool/tree/5th-multitableview)

# 実装までの流れ

- メインエリア（テーブル情報、データ情報）部分をコンポーネント化する
- テーブルを読み込む時、そのコンポーネントを動的生成する様にする

## コンポーネント化

### まず、簡単なコンポーネントを作成してみる。

公式ページに [FXMLを使用したカスタム・コントロールの作成](https://docs.oracle.com/javase/jp/8/javafx/fxml-tutorial/custom_control.htm) なるものがあった。こちらをトレースする。

- Controller javaファイル
- デザイン用 Fxmlファイル

Controller 用java のPathは「main/java/com/silverboxsoft/dynamodbtool/controller/CustomControl.java」
fxml ファイルのPathは「main/resources/com/silverboxsoft/dynamodbtool/controller/javafx/custom_control.fxml」
後者が javafx フォルダ階層が追加されてる感じ。

<details><summary>引用＋フォルダ名変更：CustomControl.java</summary><div>

```java:CustomControl.java
package com.silverboxsoft.dynamodbtool.controller;

import java.io.IOException;

import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;

public class CustomControl extends VBox {
	@FXML
	private TextField textField;

	public CustomControl() {
		FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(
				"javafx/custom_control.fxml"));
		fxmlLoader.setRoot(this);
		fxmlLoader.setController(this);

		try {
			fxmlLoader.load();
		} catch (IOException exception) {
			throw new RuntimeException(exception);
		}
	}

	public String getText() {
		return textProperty().get();
	}

	public void setText(String value) {
		textProperty().set(value);
	}

	public StringProperty textProperty() {
		return textField.textProperty();
	}

	@FXML
	protected void doSomething() {
		System.out.println("The button was clicked!");
	}
}
```

</div></details>

<details><summary>引用＋import文追加：custom_control.fxml</summary><div>

```xml:custom_control.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml">
    <TextField fx:id="textField"/>
    <Button text="Click Me" onAction="#doSomething"/>
</fx:root>
```

</div></details>

### それを既存エリアに表示してみる

- 親に設定したいコンポーネントにfxidを振る（今回は paneTableInfo）
- そのコンポーネントで、getChildren().add を使って子コンポーネントとする。

```java:DynamoDbToolController.java

	// テーブル情報表示部のコンポーネント
	@FXML
	AnchorPane paneTableInfo;

	// 中略

	@Override
	public void initialize(URL location, ResourceBundle resources) {
		initCmb();
		initLoadDialog();
		// この部分を追加
		CustomControl customControl = new CustomControl();
		customControl.setText("Hello!");
		paneTableInfo.getChildren().add(customControl);
	}
```

### こんな感じ

ベースとなるPaneとか設定していないので重なっちゃってるけどちゃんと表示された。Clickしたらコンソール出力もされた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/aeee236d-b28e-7dc3-a7bd-a6544ed1f955.png)

確認後関係ソースは削除。

## 本番（テーブル情報＋データ部のコンポーネント化）

### テーブル情報＋データ部のfxml作成

SceneBuilderにて、まず、fxmlファイルを別名（DynamoDbTable.fxml）で保存しておいて、該当部分だけ切り取り。一旦全部品削除の上、貼り付け。
同、Controllerクラス（com.silverboxsoft.dynamodbtool.controller.DynamoDbTable）指定
※この時、「Controller class」は空白に、「Use fx:root construct」チェックを外しておかないと上手く行かない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/9fe1cfaa-e2f1-c0d5-253d-5b8f3a5cdfe2.png)

その時、xmlはこんな形になってるはず。```fx:root``` タグ及びその下のchildrenで囲まれてる。

```xml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<!-- 中略 -->
<?import javafx.scene.text.*?>

<fx:root minHeight="0.0" minWidth="0.0" prefHeight="631.0" prefWidth="800.0" type="AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" >
   <children>
      <SplitPane dividerPositions="0.30842607313195547" layoutX="14.0">
          <!-- 中略 -->
      </SplitPane>
   </children>
</fx:root>

```

### Controller javaクラス作成

DynamoDbTable.javaを作成し、コンストラクタ作成。
※必要な情報の初期化もコンストラクタで渡したかったが、どうやら無引数のコンストラクタでないとFXMLLoader.loadでNoSuchMethodErrorとかのエラーが出て駄目だった。intialize関数を用意してそれを呼び出し側で呼ぶ事に。

```java:DynamoDbTable.java
	public DynamoDbTable() {
		FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(
				"javafx/DynamoDbTable.fxml"));
		fxmlLoader.setRoot(this);
		fxmlLoader.setController(this);
		try {
			fxmlLoader.load();
		} catch (IOException exception) {
			throw new RuntimeException(exception);
		}
	}
```

### 呼び出し側対応
カスタムコンポーネントのインスタンスを作成。Tabを作って、その中身にsetContentで、その作ったインスタンスを設定。TabをTabPaneに追加。

```java:DynamoDbToolController.java
	protected void actTableDecided() throws URISyntaxException {
		String tableName = lvTableList.getSelectionModel().getSelectedItem();
		DynamoDbTable dbtable = new DynamoDbTable();
		dbtable.initialize(getConnectInfo(), tableName, dialog);
		Tab newTab = new Tab();
		newTab.setText(tableName);
		newTab.setContent(dbtable);
		tabPaneTable.getTabs().add(newTab);
	}
```

## タブ削除対応

追加できるようになったので、削除機能もつけなくてはならない。メニューに「View」を追加し、アクティブタブを閉じる、アクティブ以外タブを全部閉じる機能をつける。

```java:DynamoDbToolController.java
	@FXML
	protected void actCloseActiveTab(ActionEvent ev) {
		int activeIndex = tabPaneTable.getSelectionModel().getSelectedIndex();
		if (tabPaneTable.getTabs().size() > 1) {
			tabPaneTable.getTabs().remove(activeIndex);
		}
	}

	@FXML
	protected void actCloseAllNonActiveTab(ActionEvent ev) {
		int activeIndex = tabPaneTable.getSelectionModel().getSelectedIndex();
		int tabCount = tabPaneTable.getTabs().size();
		// アクティブタブより右を閉じる
		for (int wkIdx = activeIndex + 1; wkIdx < tabCount; wkIdx++) {
			tabPaneTable.getTabs().remove(wkIdx);
		}
		// アクティブタブより左を閉じる
		for (int wkIdx = activeIndex - 1; wkIdx >= 0; wkIdx--) {
			tabPaneTable.getTabs().remove(wkIdx);
		}
	}
```

# 次回予定

現在、文字と数値しか対応してないので、DynamoDBのリストとかマップ形式の対応をする予定。

# 参考にさせて頂いたページ

## 公式ページ

[FXMLを使用したカスタム・コントロールの作成](https://docs.oracle.com/javase/jp/8/javafx/fxml-tutorial/custom_control.htm)