Compose for DesktopのWindowのチュートリアルをやってみた

Compose for DesktopのWindowのチュートリアルをやってみた。

環境

  • Compose for Desktop 1.0.0-alpha3

参考

Windowを作成する

Windowを作成するには、ComposableスコープでWindowを使用します。
Composableスコープの作成には、application関数を使用します。

Windowを表示するだけのプログラム

import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        // Content
    }
}

Windowのプロパティを変更する

WindowはComposable関数です。宣言的にプロパティを変更できます。

Windowのプロパティを変更するプログラム

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    var number by remember { mutableStateOf(0) }

    Window(onCloseRequest = ::exitApplication, title = "number = $number") {
        Button(onClick = { number++ }) {
            Text("number++")
        }
    }
}

Windowのオープンとクローズ

Windowのオープンとクローズはif文を使って実現できます。

Windowが閉じる時に行う処理は、onCloseRequestに記述します。

サブウィンドウを開くプログラム

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    var isOpenSubWindow by remember { mutableStateOf(false) }
    Window(onCloseRequest = ::exitApplication) {
        Button(onClick = { isOpenSubWindow = true }) {
            Text("Open Window")
        }
    }
    if (isOpenSubWindow) {
        Window(onCloseRequest = { isOpenSubWindow = false }, title = "SubWindow") {
            Button(onClick = { isOpenSubWindow = false }) {
                Text("Close Window")
            }
        }
    }
}

ウィンドウを閉じる時に確認ダイアログを表示するプログラム

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    var isOpenMainWindow by remember { mutableStateOf(true) }
    var isAskingToClose by remember { mutableStateOf(false) }

    if (isOpenMainWindow) {
        Window(onCloseRequest = { isAskingToClose = true }) {
            if (isAskingToClose) {
                Dialog(
                    onCloseRequest = { isAskingToClose = false },
                    title = "ウィンドを閉じますか?"
                ) {
                    Row {
                        Button(onClick = { isOpenMainWindow = false }) {
                            Text("閉じる")
                        }
                        Button(onClick = { isAskingToClose = false }) {
                            Text("閉じない")
                        }
                    }
                }
            }
        }
    }
}

ウィンドウを非表示にする

タスクトレイにウィンドウをしまうときなど、ウィンドウを非表示にする時はwindowState.isVisibleの状態を変更します。

ウィンドウを閉じるとタスクトレイにしまうプログラム

import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import kotlinx.coroutines.delay

fun main() = application {
    var isVisible by remember { mutableStateOf(true) }

    Window(
        onCloseRequest = { isVisible = false },
        visible = isVisible,
        title = "Counter",
    ) {
        var counter by remember { mutableStateOf(0) }
        LaunchedEffect(Unit) {
            while (true) {
                counter++
                delay(1000)
            }
        }
        Text(counter.toString())
    }

    if (!isVisible) {
        Tray(
            TrayIcon,
            hint = "Counter",
            onAction = { isVisible = true },
            menu = {
                Item("Show", onClick = { isVisible = true })
                Item("Exit", onClick = ::exitApplication)
            },
        )
    }
}

object TrayIcon : Painter() {
    override val intrinsicSize = Size(256f, 256f)

    override fun DrawScope.onDraw() {
        drawOval(Color(0xFFFFA500))
    }
}

singleWindowApplication関数

singleWindowApplication関数は単一ウィンドウのアプリケーションを作成するための関数です。

import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication {
    // Content
}

ウィンドウのサイズをコンテンツに合わせる

WindowSizeの一方または両方をDp.Unspecifiedにすると、ウィンドウのサイズをコンテンツのサイズに合わせた最適なサイズに調整します。

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlin.random.Random

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(width = Dp.Unspecified, height = Dp.Unspecified),
    ) {
        Column(Modifier.background(Color(0xFFEEEEEE))) {
            Row {
                Text(
                    "label 1",
                    Modifier.size(
                        Random.nextInt(100, 500).dp,
                        Random.nextInt(100, 500).dp
                    )
                        .padding(10.dp)
                        .background(Color.White)
                )
            }
        }
    }
}

ウィンドウを最大化する

ウィンドウを最大化するには、WindowStateのplacementをWindowPlacement.Maximizedにします。

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState

fun main() = application {
    val state = rememberWindowState(placement = WindowPlacement.Maximized)

    Window(onCloseRequest = ::exitApplication, state) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Checkbox(
                checked = state.placement == WindowPlacement.Maximized,
                {
                    state.placement = if (it) {
                        WindowPlacement.Maximized
                    } else {
                        WindowPlacement.Floating
                    }
                }
            )
            Text("isMaximized")
        }
    }
}

ウィンドウをフルスクリーンにする

ウィンドウをフルスクリーンにするには、WindowStateのplacementをWindowPlacement.Fullscreenにします。

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState

fun main() = application {
    val state = rememberWindowState(placement = WindowPlacement.Floating)

    Window(onCloseRequest = ::exitApplication, state) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Checkbox(
                checked = state.placement == WindowPlacement.Fullscreen,
                {
                    state.placement = if (it) {
                        WindowPlacement.Fullscreen
                    } else {
                        WindowPlacement.Floating
                    }
                }
            )
            Text("isFullscreen")
        }
    }
}

ウィンドウを最小化する

ウィンドウを最小化するには、WindowStateのisMinimizedをtrueにします。

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState

fun main() = application {
    val state = rememberWindowState(placement = WindowPlacement.Floating)

    Window(onCloseRequest = ::exitApplication, state) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Checkbox(checked = state.isMinimized, { state.isMinimized = !state.isMinimized })
            Text("isMinimized")
        }
    }
}

ウィンドウの位置の取得と変更

ウィンドウの位置は、WindowStateのpositionで取得できます。
ウィンドウの位置を変更するには、WindowStateのpositionを更新します。

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*

fun main() = application {
    val state = rememberWindowState(placement = WindowPlacement.Floating)

    Window(onCloseRequest = ::exitApplication, state) {
        Column {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text("ウィンドウの位置 X:${state.position.x} Y:${state.position.y}")
            }
            Row(verticalAlignment = Alignment.CenterVertically) {
                var inputX by remember { mutableStateOf("100") }
                var inputY by remember { mutableStateOf("200") }
                TextField(value = inputX, onValueChange = { inputX = it }, label = { Text("x:") })
                TextField(value = inputY, onValueChange = { inputY = it }, label = { Text("y:") })
                Button(onClick = {
                    val position = state.position
                    if (position is WindowPosition.Absolute) {
                        state.position = position.copy(x = inputX.toInt().dp, y = inputY.toInt().dp)
                    }
                }) {
                    Text("ウィンドウを移動する")
                }
            }
        }
    }
}

画面の中央にウィンドウを表示するプログラム

import androidx.compose.ui.Alignment
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(position = WindowPosition(Alignment.Center))
    ) {
    }
}

ウィンドウのサイズの取得と変更

ウィンドウのサイズは、WindowStateのsizeで取得できます。
ウィンドウのサイズを変更するには、WindowStateのsizeを更新します。

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState

fun main() = application {
    val state = rememberWindowState(placement = WindowPlacement.Floating)

    Window(onCloseRequest = ::exitApplication, state) {
        Column {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text("ウィンドウのサイズ Width:${state.size.width} Height:${state.size.height}")
            }
            Row(verticalAlignment = Alignment.CenterVertically) {
                var inputW by remember { mutableStateOf("400") }
                var inputH by remember { mutableStateOf("300") }
                TextField(value = inputW, onValueChange = { inputW = it }, label = { Text("Width:") })
                TextField(value = inputH, onValueChange = { inputH = it }, label = { Text("Height:") })
                Button(onClick = {
                    state.size = state.size.copy(width = inputW.toInt().dp, height = inputH.toInt().dp)
                }) {
                    Text("ウィンドウサイズを更新する")
                }
            }
        }
    }
}

ウィンドウの位置やサイズの変更を取得する

ウィンドウの位置やサイズの変更を取得するには、snapshotFlowを使用します。

参考:Compose における副作用 | Jetpack Compose | Android Developers

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

fun main() = application {
    val state = rememberWindowState()

    Window(onCloseRequest = ::exitApplication, state) {
        LaunchedEffect(state) {
            snapshotFlow { state.size }
                .onEach { size -> println("onWindowResize $size") }
                .launchIn(this)

            snapshotFlow { state.position }
                .filter {
                    it.isSpecified
                }
                .onEach { position -> println("onWindowRelocate $position") }
                .launchIn(this)
        }
    }
}

ダイアログ

ダイアログはモーダルなウィンドウです。ダイアログを閉じるまで、親ウィンドウをロックします。

Swingで実装されているダイアログを使うこともできます。(後述)

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.window.*

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        var isDialogOpen by remember { mutableStateOf(false) }

        Button(onClick = { isDialogOpen = true }) {
            Text("Open dialog")
        }

        if (isDialogOpen) {
            Dialog(
                onCloseRequest = { isDialogOpen = false },
                state = rememberDialogState(position = WindowPosition(Alignment.Center))
            ) {
                Button(onClick = { isDialogOpen = false }) {
                    Text("Close")
                }
            }
        }
    }
}

Swingの相互運用

ウィンドウを作成する

Compose for Desktopは内部でSwingを使用しています。
Swingを直接使用してウィンドウを作成することもできます。

ComposeWindowはjavax.swing.JFrameを継承したウィンドウです。
ComposeWindowを使ってUIを構築できます。

import androidx.compose.ui.awt.ComposeWindow
import java.awt.Dimension
import javax.swing.JFrame
import javax.swing.SwingUtilities

fun main() = SwingUtilities.invokeLater {
    ComposeWindow().apply {
        size = Dimension(300, 300)
        defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
        setContent {
            // Content
        }
        isVisible = true
    }
}

ComposeWindowにアクセスする

Composable Windowスコープの中で、ComposeWindowにアクセスできます。

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.window.singleWindowApplication
import java.awt.datatransfer.DataFlavor
import java.awt.dnd.DnDConstants
import java.awt.dnd.DropTarget
import java.awt.dnd.DropTargetAdapter
import java.awt.dnd.DropTargetDropEvent

fun main() = singleWindowApplication {
    LaunchedEffect(Unit) {
        window.dropTarget = DropTarget().apply {
            addDropTargetListener(object : DropTargetAdapter() {
                override fun drop(event: DropTargetDropEvent) {
                    event.acceptDrop(DnDConstants.ACTION_COPY);
                    val fileName = event.transferable.getTransferData(DataFlavor.javaFileListFlavor)
                    println(fileName)
                }
            })
        }
    }
}

AWTのダイアログを使う

AWTのダイアログを使用するには、Composable関数にラップします。

import androidx.compose.runtime.*
import androidx.compose.ui.window.AwtWindow
import androidx.compose.ui.window.application
import java.awt.FileDialog
import java.awt.Frame
import java.io.File

fun main() = application {
    var isOpen by remember { mutableStateOf(true) }

    if (isOpen) {
        FileDialog(
            title = "ファイルを選択",
            mode = FileDialog.LOAD,
            onCloseRequest = {
                isOpen = false
                println("Result ${it?.absolutePath}")
            }
        )
    }
}

@Composable
private fun FileDialog(
    parent: Frame? = null,
    title: String = "Choose a file",
    mode: Int = FileDialog.LOAD,
    onCloseRequest: (result: File?) -> Unit
) = AwtWindow(
    create = {
        object : FileDialog(parent, title, mode) {
            override fun setVisible(value: Boolean) {
                super.setVisible(value)
                if (value) {
                    val file = File(directory, file)
                    onCloseRequest(file)
                }
            }
        }
    },
    dispose = FileDialog::dispose
)

Linux Mintのショートカットキーを変更するには

環境

  • Linux Mint 20.2

問題

Linux Mintでは、Ctrl+Alt+Lキーを押すと画面がロックされる。

Ctrl+Alt+Lキーは、よく使うアプリで使用されているショートカットキーのため、画面ロックしてほしくない。

Linux Mintでショットカットキーの割当を変更したい。

方法

画面のロックのショートカットキーからCtrl+Alt+Lキーを削除する。

  1. メニューから「設定」→「キーボード」を選択する。
  2. キーボードウィンドウが表示されるので、「ショートカット」タブを選択する。
  3. 「カテゴリ」→「システム」を選択する。
  4. 「キーボードショットカット」→「画面のロック」を選択する。
  5. 「キーボード割り当て」の「Ctrl+Alt+L」をタブルクリックする。
  6. 編集状態になるので、Backspaceキーを押す。「Ctrl+Alt+L」が「未割り当て」に変わる。

以上で、設定は完了。

Ctrl+Alt+Lキーを押しても画面はロックされなくなった。

Compose For DesktopのWindow関数がDeprecatedの警告の対策

環境

  • Compose for Desktop 1.0.0-alpha3

問題

Compose for Desktopのプロジェクトを作ると、Window関数がDeprecatedの警告が表示される。

対策

新しいWindow APIを使用する。

importするWindowを変更する。applicationもimportする。

//import androidx.compose.desktop.Window
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

Window関数の呼び出しを以下のように変更する。

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        ...
    }
}

全体のソースコードは次のようになる。

import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        var text by remember { mutableStateOf("Hello, World!") }

        MaterialTheme {
            Button(onClick = {
                text = "Hello, Desktop!"
            }) {
                Text(text)
            }
        }
    }
}

VirtualBoxの仮想ディスク(vdiファイル)を圧縮してファイルサイズを減らす

Vagrantで使用しているVirtualBoxの仮想ディスクを圧縮する。
ホストOSはmacOS、ゲストOSはCentOS7。

(1) ゲストOSで以下のコマンドを実行する

$ sudo dd if=/dev/zero of=/zero bs=4k
dd: `/zero' の書き込みエラー: デバイスに空き領域がありません
20656780+0 レコード入力
20656779+0 レコード出力
84610166784 バイト (85 GB) コピーされました、 72.4124 秒、 1.2 GB/秒
$ sudo rm /zero

(2) ゲストOSを終了する。

vagrant halt

(3) ホストOSで圧縮するvdiファイルのUUIDを確認する

vboxmanage list hdds

(4) 仮想ディスク(vdiファイル)を圧縮する

vboxmanage modifyhd UUID --compact