Jetpack Compose for Desktop チュートリアル紹介

普段はAndroidエンジニアをやってる[@sada](https://twitter.com/sadashi_ota)です。

先月、Jetpack Compose for Desktopが登場しました！

このプロジェクトはAndroid向けUIフレームワークのJetpack Composeを元にしていて、macOS/Linux/Windows上で動作するアプリケーションをFlutterやReactのような宣言的UIで作ることができます。
Androidエンジニアとしては全部Kotlinでかけるのは楽しいですね！

公式の関連ブログはこちら

- [Jetpack Compose for Desktop: Milestone 1 Released](https://blog.jetbrains.com/cross-post/jetpack-compose-for-desktop-milestone-1-released/)
- [Jetpack Compose for Desktop: Milestone 2 Released](https://blog.jetbrains.com/cross-post/jetpack-compose-for-desktop-milestone-2-released/)

試しに動かしたい場合は[Getting Started](https://github.com/JetBrains/compose-jb/blob/master/tutorials/Getting_Started/README.md)にチュートリアルがあるので、そちらを参考にすると良いと思います。

それではここからは実際のサンプルコードを紹介したいと思います。

# 動作確認環境

- macOS Catalina 10.15.7
- IntelliJ IDEA 2020.3 (Community Edition)
- AdoptOpenJDK 14.0.2
- Kotlin 1.4.20
- Desktop Compose 0.2.0-build132

# 画像表示

以下のように`Image`を使って画像を表示します。

```kotlin
fun main() {
    Window {
        Image(
            bitmap = TODO("ImageBitmapを生成する"),
            modifier = Modifier.fillMaxSize()
        )
    }
}
```

`ImageBitmap`の作り方は以下のような方法があります。

## リソースから読み込む

`imageResource`という関数を利用してリソース内の画像を表示します。

```kotlin
bitmap = imageResource("sample.png"),
```

こちらは`@Composable`関数なので、同様に`@Composable`関数内でしか呼べない点は注意しましょう。
また、xmlのvector imageも利用することができます。その場合は`bitmap`ではなく`imageVector`に`vectorXmlResource`関数で取得した`VectorImage`を設定しましょう。

```kotlin
imageVector = vectorXmlResource("images/compose-logo.xml"),
```

## デバイスストレージから読み込む

表示したい画像のパスを指定して`File`から`ImageBitmap`を生成します。

```kotlin
bitmap = Image.makeFromEncoded(File("sample.png").readBytes()).asImageBitmap(),
```

## その他の方法

`Canvas`を利用してrowデータを描画する方法もありますが、こちらは長くなるので割愛します。

# アプリケーションアイコン

Window生成時に`BufferedImage`を生成して`icon`に指定するだけで表示されます。

```kotlin
fun main() {
    Window(icon = ImageIO.read(File("sample.png"))) {
    }
}
```

# トレイアイコン

`onActive`で`Tray`を生成して、`icon`に設定するだけです。
`menu`も設定しないと表示されないのでご注意ください。

```kotlin
fun main() {
    Window {
        onActive {
            val tray = Tray().apply {
                icon(ImageIO.read(File("sample.png")))
                menu(
                    MenuItem(
                        name = "Quit App",
                        onClick = { AppManager.exit() }
                    )
                )
            }
            onDispose {
                tray.remove()
            }
        }
    }
}
```

# スクロールバー

スクロールバーを表示するなら必要最低限のコードだとこんな感じです。

```kotlin
fun main() {
    Window {
        Box {
            val stateVertical = rememberScrollState(0f)
            val stateHorizontal = rememberScrollState(0f)

            ScrollableColumn(scrollState = stateVertical) {
                ScrollableRow(scrollState = stateHorizontal) {
                    Column {
                        for (item in 0..100) {
                            Box { Text("Item in ScrollableColumn #$item") }
                        }
                    }
                }
            }
            VerticalScrollbar(adapter = rememberScrollbarAdapter(stateVertical))
            HorizontalScrollbar(adapter = rememberScrollbarAdapter(stateHorizontal))
        }
    }
}
```

するべきこととしては以下の４つです。

- `rememberScrollState()`で各スクロールバーの状態を保持する`ScrollState`を生成する
- `ScrollableColumn`や`ScrollableRow`の引数`scrollState`に生成した`ScrollState`を渡す
- `rememberScrollbarAdapter(scrollState: ScrollState)`で`ScrollbarAdapter`を生成する
- `Scrollbar`の引数`adapter`に生成した`ScrollbarAdapter`を渡す


# マウスイベント

## クリック関連イベント

`Modifier.clickable()`にリスナーを設定することで実装できます。

```kotlin
Box(
    modifier = Modifier.clickable(
        onClick = {
        },
        onDoubleClick = {
        },
        onLongClick = {
        }
    )
)
```

## マウス移動イベント

`Modifier.pointerMoveFilter()`にリスナーを設定することで実装できます。

- `onMove`: マウス座標の取得
- `onEnter`: 該当のViewのマウスが進入
- `onExit`: 該当のViewからマウスが退出


```kotlin
Box(
    modifier = Modifier.pointerMoveFilter(
        onMove = {
            false
        },
        onEnter = {
            false
        },
        onExit = {
            false
        }
    )
)
```

# キーイベント


以下のサンプルはテキストフィールドに入力した文字をCmd+Enterで「Appl
y text: 」に反映し、Cmd+Rで入力項目共々リセットするようにしています。
KeyInutに関してはまだExperimentalなので今後I/Fが大きく変わる可能性があることをご留意ください。

```kotlin
@OptIn(ExperimentalKeyInput::class)
fun main() = Window {
    MaterialTheme {
        var applyText by remember { mutableStateOf("") }
        var inputText by remember { mutableStateOf("") }
        Column {
            Text("Apply text: $applyText")
            TextField(
                value = inputText,
                onValueChange = { inputText = it },
                modifier = Modifier.shortcuts {
                    on(Key.CtrlLeft + Key.Enter) {
                        applyText = inputText
                        inputText = ""
                    }
                    on(Key.CtrlLeft + Key.R) {
                        applyText = ""
                        inputText = ""
                    }
                }
            )
        }
    }
}
```

また、`AppWindow`には`keyboard`プロパティがあり、現在のウィンドウで常にアクティブになるキーボードショートカットを定義することができます。
以下は、ポップアップWindowにEscapeでクローズする処理を割り当てたサンプルです。

```kotlin
AppWindow().also {
    it.keyboard.setShortcut(Key.Escape) {
        it.close()
    }
}.show {
}
```

# Java Swing フレームワークを使う

Swingを利用する場合は、`ComposePanel`を生成してSwingレイアウトに追加します。
最低限のコードサンプルはこんな感じになります。

```kotlin
fun main() {
    val window = JFrame()
    val composePanel = ComposePanel()
    window.contentPane.add(composePanel, BorderLayout.CENTER)
    composePanel.setContent {
        Box {
            Text("text")
        }
    }
    window.setSize(100, 100)
    window.isVisible = true
}
```

# まとめ

いろいろなサンプルをとりあげてみましたがいかがでしょうか？
この記事で紹介したサンプルは以下のチュートリアルを参考にしています。

- https://github.com/JetBrains/compose-jb/tree/master/tutorials

Androidエンジニアの方は、デスクトップアプリもちょっと作ってみようかな？ってなりませんか？？（自分はなりましたw）
KotlinはAndroidだけでなくiOSやサーバーサイド、デスクトップ（Linux,macOS,Windows）など様々なプラットフォームに対応しています。
Kotlinを起点に色々なプラットフォームに手を出してみるのも面白いかもしれませんね。