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
)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください