Kotlin + Swing をちょっと楽にする

JDK と JavaFX の問題について考えるのが面倒になってしまったので、最近は Swing を使って GUI プログラミングをしています。Kotlin の機能を使って簡単にかけるので案外楽かもしれません。

## 作るもの
ボタンをクリックすると ラベルの値が true/false で切り替わるだけのシンプルなものです。

## 1. Java Like
### ソースコード
```kotlin
fun main() {
    val frame = JFrame()
    frame.bounds = Rectangle(150, 150)
    frame.setLocationRelativeTo(null)
    val panel = JPanel()
    var toggleState = false
    val label = JLabel(toggleState.toString())
    val button = JButton("Click")
    button.addActionListener {
        toggleState = !toggleState
        label.text = toggleState.toString()
    }
    panel.add(label)
    panel.add(button)
    frame.add(panel)
    frame.isVisible = true
}
```

見ての通り、普通なコードになります。変数として `JFrame`, `JLabel` などを定義して使っています。

## 2. スコープ関数を使う ⑴
### ソースコード
```kotlin
fun main() {
    JFrame().apply { 
        bounds = Rectangle(150, 150)
        setLocationRelativeTo(null)
        val panel = JPanel().apply {
            var toggleState = false
            val label = JLabel(toggleState.toString())
            val button = JButton("Click").apply {
                addActionListener {
                    toggleState = !toggleState
                    label.text = toggleState.toString()
                }
            }
            add(label)
            add(button)
        }
        add(panel)
        isVisible = true   
    }
}
```

`panel.add` などと書いていた部分が `apply { }` に変わっています。しかし、これだけでは有り難みがあまりないです。

## 3. スコープ関数を使う ⑵
### おはなし
```kotlin
val panel = JPanel().apply { /* ... */ }
/* ... */
add(panel)
```

以上のようなコードを以下のように書ければ少し楽になりそうですが、一つ問題があります。

```kotlin
add(JPanel().apply { /* ... */ })
```

しかし、`Container::add` の戻り値は `Component` なので、以下のようなコードではエラーが出てしまいます。

```kotlin
val label/* : Component */ = add(JLabel())
label.text = "" // Error
```

なので、キャストをしてあげる必要があります。

```kotlin
val label/* : JLabel */ = add(JLabel()) as JLabel
label.text = "" // OK
```

この前提をもとに書き換えてみましょう。

### ソースコード
```kotlin
fun main() {
    JFrame().apply {
        bounds = Rectangle(150, 150)
        setLocationRelativeTo(null)
        add(JPanel().apply {
            var toggleState = false
            val label = add(JLabel(toggleState.toString())) as JLabel
            add(JButton("Click").apply {
                addActionListener {
                    toggleState = !toggleState
                    label.text = toggleState.toString()
                }
            })
        })
        isVisible = true
    }
}
```

よくはなりましたが、 何度もキャストを書くのは面倒です。

## 4. Container::add の改善
### おはなし

`add(JLabel)` なら 戻り値を JLabel にするようにしてしまいましょう。
そのために [Generics](https://kotlinlang.org/docs/reference/generics.html#generic-functions) というものが使えます。 

#### キャストを使う(非推奨)

```kotlin
fun <T: Component> Container.addT(component: T) = add(component) as T
```

しかし、この関数では `Unchecked cast: Component! to T` という警告が出てしまいます。ジェネリクス関数では呼び出し時に型情報が消えてしまうので、このような警告が発生してしまいます。そのために、この処理をインライン展開させてしまいましょう。

```kotlin
inline fun <reified T: Component> Container.addT(component: T) = add(component) as T
```

`reified` というパラメータをつけることで、型情報が保持されるので警告は出ません。

#### スコープ関数を使う
@sdkei さんより。
キャストを使うと型安全性が落ちてしまう危険性があるので、避けられるならば避けるようにしましょう。今回の場合、スコープ関数を使うことで避けることができます。

```kotlin
fun <T: Component> Container.addT(component: T) = component.apply { add(this) }
```

こちらの方が良さそうですね。

### ソースコード
```kotlin
fun <T: Component> Container.addT(component: T) = component.apply { add(this) }

fun main() {
    JFrame().apply {
        bounds = Rectangle(150, 150)
        setLocationRelativeTo(null)
        add(JPanel().apply {
            var toggleState = false
            val label = addT(JLabel(toggleState.toString()))
            add(JButton("Click").apply {
                addActionListener {
                    toggleState = !toggleState
                    label.text = toggleState.toString()
                }
            })
        })
        isVisible = true
    }
}
```

## 5. 拡張関数を定義する
```kotlin
inline fun jFrame(action: JFrame.() -> Unit) = JFrame().apply(action)
inline fun Container.jPanel(action: JPanel.() -> Unit = {}) = addT(JPanel().apply(action))
inline fun Container.jLabel(text: String, action: JLabel.() -> Unit = {}) = addT(JLabel(text).apply(action))
inline fun Container.jButton(text: String, action: JButton.() -> Unit = {}) = addT(JButton(text).apply(action))
```

このような拡張関数を定義してしまいましょう。関数の内部で `addT` を実行しているので、外部で呼び出す必要はないです。

### ソースコード
```kotlin
fun <T: Component> Container.addT(component: T) = component.apply { add(this) }
inline fun jFrame(action: JFrame.() -> Unit) = JFrame().apply(action)
inline fun Container.jPanel(action: JPanel.() -> Unit = {}) = addT(JPanel().apply(action))
inline fun Container.jLabel(text: String, action: JLabel.() -> Unit = {}) = addT(JLabel(text).apply(action))
inline fun Container.jButton(text: String, action: JButton.() -> Unit = {}) = addT(JButton(text).apply(action))

fun main() {
    jFrame {
        bounds = Rectangle(150, 150)
        setLocationRelativeTo(null)
        jPanel {
            var toggleState = false
            val label = jLabel(toggleState.toString())
            jButton("Click") {
                addActionListener {
                    toggleState = !toggleState
                    label.text = toggleState.toString()
                }
            }
        }
        isVisible = true
    }
}
```

Kotlin の機能を活かすことで Swing も綺麗にかけるわけですね。