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
)