Compose for Desktop でQRコードをリアルタイム生成するアプリケーションを作ってみた【Kotlin】

こんにちは。

先日Compose for Desktop を用いたデスクトップアプリ開発に入門しました。
その際にQRコードの生成と読み取りを行うアプリケーションを作成したので、その作成手順などを共有します。

よろしくお願いします。

# Compose for Desktop について

この記事を見ている人はすでに知っているフレームワークだと思いますが、念のため。

Compose for DesktopはJetBrainsが開発するデスクトップアプリを作成するためのフレームワークです。
Webアプリも作れるみたいです。

詳細はこちらから。

https://www.jetbrains.com/ja-jp/lp/compose-mpp/

# 作ったもの

以下のGifにあるように入力した文字をリアルタイムでQRコードに変換してくれるデスクトップアプリです。

![2022-12-01_22-47-26.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447932/eca84014-4ca2-826f-49ec-cfddf44d07fe.gif)

ちなみに、デスクトップやカメラからもQRコードを読み取ることができます。
ただ、今回はあくまでリアルタイムにQRコードを作成するアプリの作り方なのでこれについては省略します。

![012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447932/83a0e013-da97-a04b-1875-12a75c2eeb7a.png)

ソースコードは以下のリポジトリで公開しています。

https://github.com/rokuosan/QRCode-Tools

# 開発環境

以下の環境で開発を行いました。
OSが写真ではWindows11になっていますが、開発段階ではWindows10を利用していました。

| Name | Version |
|:- | :- |
| OS | Windows 10 |
| JDK | Amazon Corretto 11 |
| Kotlin | 1.6.10 |
| Gradle | 7.4.2 |

# 使用ライブラリ

| Name | Version | Description |
| :- | :- | :- |
| Compose for Desktop | 1.1.0 | 今回の主役 |
| ZXing | 3.5.0 | QRコードを作成・読み取りするためのライブラリ |
| Webcam Capture | 0.3.12 | Java でウェブカメラを使用するためのライブラリ |
| ShadowJar | 7.1.2 | おなじみのFarJarを作成するためのライブラリ |

# 作成手順

## 1. プロジェクトの作成

プロジェクトの作成方法については特にいうことがないので割愛。

## 2. QRコードとテキストの相互変換機能の作成

### QRコードの画像生成

QRコードに変換したいテキストを引数にもつ関数を定義します。

ここでのポイントはhintsで文字コードと余白のピクセルを指定するところです。

ZXingのデフォルトで利用される文字コードは日本語をサポートしません。
そのため、日本語を含むテキストを正常にエンコードできるようにする必要があります。

また、QRコードを読み取るためには黒い部分の周りにある余白が重要になります。
もしここで生成したQRコードを別の白背景を準備するなら4以下を指定しても構いませんが、通常の場合は最低でも4以上のピクセルを指定してください。
そうでないと正常に読み取れません。

```kotlin
object Generator {
    private val writer = QRCodeWriter()

    fun makeQRCode(text: String, width: Int = 512, height: Int = 512): BufferedImage{
        val hints: MutableMap<EncodeHintType, Any> = EnumMap(EncodeHintType::class.java)
        hints[EncodeHintType.ERROR_CORRECTION]=ErrorCorrectionLevel.L // 誤り訂正レベルを指定
        hints[EncodeHintType.CHARACTER_SET]=Charsets.UTF_8.displayName() // 文字コードを指定
        hints[EncodeHintType.MARGIN]=4 // 余白のピクセル

        return MatrixToImageWriter.toBufferedImage(writer.encode(text, BarcodeFormat.QR_CODE, width, height, hints))
    }
}
```

## 3. ウィンドウの作成

次にウィンドウを作成します。

```kotlin
@Composable
fun GeneratorWindow(){
    var text by remember { mutableStateOf("") }
    var rawImage: BufferedImage? by remember { mutableStateOf(null) }
    var image by remember { mutableStateOf( makeQRCode("Placeholder").toPainter()) }
    val focus = remember { FocusRequester() }

    Column{
        // タイトルバー
        TopAppBar(
            contentPadding = PaddingValues(horizontal = 16.dp)
        ){
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text("QRCode Generator", fontWeight = FontWeight.SemiBold)

                // 画像保存ボタン
                OutlinedButton(
                    onClick = { saveDialog(rawImage) },
                    border = BorderStroke(2.dp, Color.White),
                    colors = ButtonDefaults.outlinedButtonColors(
                        backgroundColor = Color.Transparent,
                        contentColor = Color.White
                    )
                ){
                    Text("Save Image")
                }
            }
        }

        Row(
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {

            // テキストフィールド
            OutlinedTextField(
                value = text,
                onValueChange = { // 値が変わったときの処理
                    text = it
                    if(text.isNotEmpty()){
                        rawImage = makeQRCode(text)
                        image = rawImage!!.toPainter()
                    }
                },
                modifier = Modifier.width(200.dp)
                    .fillMaxHeight()
                    .focusRequester(focus),
                placeholder = { Text("ここにQRコードに変換したい文字列を入力してください") },
                shape = RectangleShape,
                colors = TextFieldDefaults.outlinedTextFieldColors(
                    focusedBorderColor = Color.Gray
                )
            )


            // テキストウィンドウにフォーカスさせる
            LaunchedEffect(Unit){
                focus.requestFocus()
            }


            // 生成したQRコード
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center,
            ){
                Image(
                    image,
                    "",
                    contentScale = ContentScale.Fit
                )
            }
        }
    }
}

private fun saveDialog(image: BufferedImage?){
    // 省略
}
```

Compose for Desktopではテキストエリアに対して値が変わるごとに実行される処理を定義することができます。

一文字ずつQRコードに変換して、それを画像として表示するサイクルを繰り返すだけの処理です。

画像の保存に関してはここでは省略しましたが、JFileChooserを用いた実装を行っています。

特に使うわけでもないので、これはSwingの力を借りています。

Compose for Desktop はSwingやAWTと同時に利用することができるため、このようにSwing等ですでに実装されているものも活用することができます。

# おまけ: マルチウィンドウについて

今回このアプリケーションを作成するとき、最も躓いたポイントがこのマルチウィンドウ表示です。

これについて以前Zennでスクラップとして書いたのでそちらを参照してください。

もっともよい方法かどうかはわかりませんが、以下のようにすることで複数のウィンドウを同時に開くことができます。

https://zenn.dev/rokuo/scraps/4a40693f61bb53

# おわりに

最初こそ躓いたものの、全体的には割とスムーズに書くことができました。

Swingと比べてCompose for DesktopはよりKotlinらしさを感じることができて圧倒的に楽しかったです。

# ソースコード

https://github.com/rokuosan/QRCode-Tools