JavaFXのListViewで独自のレイアウトを表示させたり、タブ毎にデータを管理したい時にやったこと

## はじめに
KotlinとJavaFXでタスク管理をするGUIアプリケーションを作ろうとした際に以下のようなことをしたいと思いました。

- 複数のタブを作成できるようにした上でタブ毎にタスクを管理したい

- 表示するタスクは独自のレイアウトを使用して表示したい

この記事では、タブ毎にリストで表示するデータを保持する方法と背景を透過させてタスクを表示させる方法、ListViewのセルをカスタムする方法について書きたいと思います。

## 複数のタブ毎にデータを持たせる実装方法
タブ毎にデータを持たせるには、fxml側でタブ毎にcontrollerとして値を保持するクラスを設定するだけです。

```xml:tab.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<AnchorPane id="tabAnchorPane" 
            stylesheets="@../css/task_tab.css" 
            xmlns="http://javafx.com/javafx/8.0.121"
            xmlns:fx="http://javafx.com/fxml/1" 
            fx:controller="controller.TabController">
    <VBox>
        <TextField id="textField"
                   fx:id="textField"
                   onKeyPressed="#onKeyPress"
                   prefHeight="20"
                   prefWidth="800"/>

        <ListView id="listView"
                  fx:id="listView"
                  layoutY="27.0"
                  prefHeight="440"
                  prefWidth="800"/>
    </VBox>
</AnchorPane>
```
例ではTextFieldに入力された値をlistViewで表示するリストに追加しています。
fxml側で`fx:controller="TabController`のプロパティを設定することで、読み込まれたfxml毎にTabControllerのインスタンスを持つことになるため、タブ毎に異なるデータを保持することができます。

```Kotlin:TabController.kt
class TabController {

    @FXML
    private lateinit var textField: TextField
    @FXML
    private lateinit var listView: ListView<String>

    private val listItem: ObservableList<String> = FXCollections.observableArrayList()

    fun initialize() {
        listView.items = listItem
    }

    @FXML
    fun onKeyPress(key: KeyEvent) {
        if (key.code == KeyCode.ENTER && textField.text.isNotEmpty()) {
            listItem.add(textField.text)
            textField.clear()
        }
    }
}
```

## ListViewで表示するタスクを独自のレイアウトで表示する際に行ったこと


- ListViewに何もせずにデータをセットすると、セルの色がデフォルトで交互に変わるのでCSSでセルの色を変更した。
- ListViewで表示するcellにfxmlを使用して自作のレイアウトを設定した。

### セルのバックグラウンドカラーを変更する
まず、タブのコンテンツとして作成したfxmlに対してCSSを読み込むプロパティを設定します。

```xml:tab.fxml
<AnchorPane id="tabAnchorPane" 
            stylesheets="@../css/cell.css" 
            ... >

```

CSSにはセルの設定を記述します。
セルの背景色を設定する方法としては、[こちら](https://stackoverflow.com/questions/33454279/listview-styling-in-javafx)をそのまま参考にすることで解決できました。
適用するセルのレイアウトが背景色に影響されないように参考にしたサイトをそのままに背景色を透明にしました。

```css:cell.css
.listView .list-cell:even {
    -fx-background-color: transparent;
}

.listView .list-cell:odd {
    -fx-background-color: transparent;
}
```

### セルに独自のレイアウトを設定する
セルをカスタマイズする方法は[こちら](https://aoe-tk.hatenablog.com/entry/20131206/1386345344)と[こちら](http://www.takaiwa.net/2014/04/javafxlistviewrowfxml.html)を参考にしました。
詳しいことはリンクを見ていただけれ分かるかと思いますので、ここではどう実装したかだけを書きます。

まず、セルで表示するレイアウトを作成します。
今回はシンプルにLabelを表示するだけです。

```xml:layout_task_cell.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
            id="taskPane"
            fx:id="taskPane"
            xmlns:fx="http://javafx.com/fxml"
            prefHeight="20.0" prefWidth="600.0">

    <Label id="task"
           fx:id="task"/>

</AnchorPane>
```

今回はListViewのセルをカスタムするので、ListCellクラスを継承したクラスを作成します。
FXMLLoaderで作成したレイアウトを読み込み、コントローラーとしてカスタムセルクラスを渡します。
```updateItem（）```の中でレイアウトの要素に値を設定していきます。

今回はレイアウトの要素として```Label```のみなので、```ListCell<String>```としていますが```ListCell<DataModel>```といった感じでも書けるので、データをまとめたクラスを渡すこともできます。


```Kotlin:CustomTaskCell.kt
import javafx.fxml.FXML
import javafx.fxml.FXMLLoader
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.control.ListCell
import javafx.scene.layout.AnchorPane
import util.AppResource

class CustomTaskCell : ListCell<String>() {
    @FXML
    private lateinit var task: Label
    @FXML
    private lateinit var taskPane: AnchorPane

    // pathの部分は適宜環境に合わせて書く
    private val loader : FXMLLoader = FXMLLoader(javaClass.classLoader.getResource("../resources/layout/layout_task_cell.fxml"))

    init {
        loader.setController(this)
        loader.load<Node>()
    }

    override fun updateItem(item: String?, empty: Boolean) {
        super.updateItem(item, empty)

        if (empty) {
            this.task.text = null
            graphic = null
        } else {
            item?.let {
                this.task.text = it
            }
            graphic = taskPane
        }
    }
}
```

最後にカスタムセルクラスをListViewに設定します。

```Kotlin:TabController.kt
class TabController {

    ....

    fun initialize() {
        ....
        listView.cellFactory = Callback<ListView<String>, ListCell<String>> {
            CustomTaskCell()
        }
    }
    ....
}
```

## おわりに
当初、タブ毎にデータを管理したり、ListViewのセルをカスタムしたりするのは、それほど難しくないのではないかと思っていた。
しかし、実際に実装してみると思ったように動かなかったりして結構詰まったりして勉強になった。