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