LoginSignup
3
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-01

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

作るもの

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

1. Java Like

ソースコード

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. スコープ関数を使う ⑴

ソースコード

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. スコープ関数を使う ⑵

おはなし

val panel = JPanel().apply { /* ... */ }
/* ... */
add(panel)

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

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

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

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

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

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

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

ソースコード

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 というものが使えます。

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

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

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

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

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

スコープ関数を使う

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

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

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

ソースコード

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. 拡張関数を定義する

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 を実行しているので、外部で呼び出す必要はないです。

ソースコード

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 も綺麗にかけるわけですね。

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1