해당 글은 Jetbrains에서 제공해 준 Desktop-specific components and events 글을 한글로 번역한 글입니다.
macOS, Linux 및 Windows 데스크톱 응용 프로그램에서 Compose Multiplatform을 사용할 수 있습니다. 이 글에서는 Desktop 전용 Composable 및 이벤트에 대해 간략하게 설명합니다. 각 세션에는 자세한 튜토리얼 링크가 포함되어 있습니다.
Window 컴포저블을 사용하여 일반 창을 만들고, DialogWindow 를 이용하여 Modal 창을 만들 수 있습니다. DialogWindow 는 해당 창을 닫기 전까지 상위 Window 컴포저블을 잠글 수 있습니다.
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
) {
var isDialogOpen by remember { mutableStateOf(false) }
Button(onClick = { isDialogOpen = true }) {
Text(text = "Open dialog")
}
if (isDialogOpen) {
DialogWindow(
onCloseRequest = { isDialogOpen = false },
state = rememberDialogState(position = WindowPosition(Alignment.Center))
) {
// Dialog's content
}
}
}
}
Compose Multiplatform은 Window에 대한 다양한 기능을 제공합니다. 크기를 조정하거나, Window의 상태를 조절하거나, 드래그 가능하게 만들 수도 있고, 창을 투명하게 만들 수도 있습니다.
Context Menus는 기본적으로 TextField 컴포저블 (및 선택 가능한 Text 컴포저블)에 지원됩니다.
fun main() = singleWindowApplication(title = "Context menu") {
val text = remember { mutableStateOf("Hello!") }
Column {
ContextMenuDataProvider(
items = {
listOf(
ContextMenuItem("User-defined Action") {/*do something here*/ },
ContextMenuItem("Another user-defined action") {/*do something else*/ }
)
}
) {
TextField(
value = text.value,
onValueChange = { text.value = it },
label = { Text(text = "Input") }
)
Spacer(Modifier.height(16.dp))
SelectionContainer {
Text("Hello World!")
}
}
}
}
기본적인 컨텍스트 메뉴에는 복사, 잘라내기, 붙여넣기 및 모두 선택 기능을 제공합니다.
더 많은 메뉴 항목을 추가하고, 커스터마이징 할 수 있습니다.
Tray 컴포저블을 사용하여 시스템 트레이에 알림을 보내는 기능을 구현할 수 있습니다.
fun main() = application {
var count by remember { mutableStateOf(0) }
var isOpen by remember { mutableStateOf(true) }
if (isOpen) {
val trayState = rememberTrayState()
val notification = rememberNotification("Notification", "Message from MyApp!")
Tray(
state = trayState,
icon = TrayIcon,
menu = {
Item(
"Increment value",
onClick = {
count++
}
)
Item(
"Send notification",
onClick = {
trayState.sendNotification(notification)
}
)
Item(
"Exit",
onClick = {
isOpen = false
}
)
}
)
Window(
onCloseRequest = {
isOpen = false
},
icon = MyAppIcon
) {
// Content:
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Value: $count")
}
}
}
}
object MyAppIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height))
drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f))
drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f))
}
}
object TrayIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color(0xFFFFA500))
}
}
알림은 세 가지 유형이 있습니다.
시스템 트레이에 어플리케이션 알림을 추가할 수도 있습니다.
MenuBar 컴포저블을 이용하여 특정 창에 메뉴 표시줄을 만들고 커스텀할 수 있습니다.
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var action by remember { mutableStateOf("Last action: None") }
var isOpen by remember { mutableStateOf(true) }
if (isOpen) {
var isSubmenuShowing by remember { mutableStateOf(false) }
Window(onCloseRequest = { isOpen = false }) {
MenuBar {
Menu("File", mnemonic = 'F') {
Item("Copy", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true))
Item(
"Paste",
onClick = { action = "Last action: Paste" },
shortcut = KeyShortcut(Key.V, ctrl = true)
)
}
Menu("Actions", mnemonic = 'A') {
CheckboxItem(
"Advanced settings",
checked = isSubmenuShowing,
onCheckedChange = {
isSubmenuShowing = !isSubmenuShowing
}
)
if (isSubmenuShowing) {
Menu("Settings") {
Item("Setting 1", onClick = { action = "Last action: Setting 1" })
Item("Setting 2", onClick = { action = "Last action: Setting 2" })
}
}
Separator()
Item("About", icon = AboutIcon, onClick = { action = "Last action: About" })
Item("Exit", onClick = { isOpen = false }, shortcut = KeyShortcut(Key.Escape), mnemonic = 'E')
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = action)
}
}
}
}
object AboutIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color(0xFFFFA500))
}
}
스크롤이 필요한 부분에 Scrollbars 컴포저블을 이용할 수 있습니다.
fun main() = singleWindowApplication(
title = "Scrollbars",
state = WindowState(width = 250.dp, height = 400.dp)
) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color(180, 180, 180))
.padding(10.dp)
) {
val stateVertical = rememberScrollState(0)
val stateHorizontal = rememberScrollState(0)
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
) {
Column {
for (item in 0..30) {
TextBox("Item #$item")
if (item < 30) {
Spacer(modifier = Modifier.height(5.dp))
}
}
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(end = 12.dp),
adapter = rememberScrollbarAdapter(stateHorizontal)
)
}
}
@Composable
fun TextBox(text: String = "Item") {
Box(
modifier = Modifier.height(32.dp)
.width(400.dp)
.background(color = Color(200, 0, 0, 20))
.padding(start = 10.dp),
contentAlignment = Alignment.CenterStart
) {
Text(text = text)
}
}
예를 들어 스크롤을 필요로 하는 컴포저블을 VerticalScrollbar 에 연결하여 사용할 수 있습니다
TooltipArea 컴포저블을 사용하여, 필요로 하는 컴포저블에 Tooltip을 추가할 수 있습니다.
TooltipArea 는 Box 위에서 동작하는 것처럼 사용할 수 있습니다.
TooltipArea(tooltip = { Surface({...}
@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Tooltip Example",
state = rememberWindowState(width = 300.dp, height = 300.dp)
) {
val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F")
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
buttons.forEachIndexed { index, name ->
// Wrap the button in BoxWithTooltip
TooltipArea(
tooltip = {
// Composable tooltip content:
Surface(
modifier = Modifier.shadow(4.dp),
color = Color(255, 255, 210),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = "Tooltip for ${name}",
modifier = Modifier.padding(10.dp)
)
}
},
modifier = Modifier.padding(start = 40.dp),
delayMillis = 600, // In milliseconds
tooltipPlacement = TooltipPlacement.CursorPoint(
alignment = Alignment.BottomEnd,
offset = if (index % 2 == 0) DpOffset(-16.dp, 0.dp) else DpOffset.Zero // Tooltip offset
)
) {
Button(onClick = {}) { Text(text = name) }
}
}
}
}
}
TooltipArea 컴포저블의 주요 매개변수는 TooltipArea다음과 같습니다.
tooltip : 툴팁 설명을 위한 컴포저블입니다.tooltipPlacement : 툴팁의 위치를 정의합니다. 앵커(마우스 커서 또는 구성 요소), 툴팁의 위치를 나타내는 offset, 나타나는 방향 등을 지정할 수 있습니다.delay : 툴팁이 표시되기까지의 시간(밀리초)입니다. 기본값은 500ms입니다.클릭, 이동, 스크롤, 입력, 종료 등 여러가지 마우스 이벤트를 감지할 수 있습니다.
Modifier를 이용해, 여러 가지 클릭 리스너를 설정할 수 있습니다.
fun main() = singleWindowApplication {
var count by remember { mutableStateOf(0) }
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
var text by remember { mutableStateOf("Click magenta box!") }
Column {
@OptIn(ExperimentalFoundationApi::class)
Box(
modifier = Modifier
.background(Color.Magenta)
.fillMaxWidth(0.7f)
.fillMaxHeight(0.2f)
.combinedClickable(
onClick = {
text = "Click! ${count++}"
},
onDoubleClick = {
text = "Double click! ${count++}"
},
onLongClick = {
text = "Long click! ${count++}"
}
)
)
Text(text = text, fontSize = 40.sp)
}
}
}
onKeyEvent 와 onPreviewKeyEvent 를 이용하여 키보드 이벤트 핸들러를 설정할 수 있습니다.
onPreviewKeyEvent 를 사용하여 특정 키들의 동작을 정의할 수 있습니다.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
MaterialTheme {
var consumedText by remember { mutableStateOf(0) }
var text by remember { mutableStateOf("") }
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Text("Consumed text: $consumedText")
TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.onPreviewKeyEvent {
when {
(it.isCtrlPressed && it.key == Key.Minus && it.type == KeyEventType.KeyUp) -> {
consumedText -= text.length
text = ""
true
}
(it.isCtrlPressed && it.key == Key.Equals && it.type == KeyEventType.KeyUp) -> {
consumedText += text.length
text = ""
true
}
else -> false
}
}
)
}
}
}
또한 Window , singleWindowApplication, Dialog Composable에 대해서 해당 Composable 위에서 항상 활성화되는 키보드 이벤트 핸들러를 정의할 수 있습니다.
Jetpack Compose에서는 Tab 키의 조합을 이용하여 다음 컴포넌트 / 이전 컴포넌트로 이동할 수 있는 로직을 작성할 수 있습니다.
기본적으로 Tabbing Navigation은 focusable한 컴포넌트를 찾아 넘어갈 수 있습니다. Focusable한 Composable은 TextField, OutlinedTextField, BasicTextField 뿐만 아니라 Modifier.clickable 을 사용하는 버튼 종류의 Composable들도 포함됩니다.
다음은 Tab 키를 사용하여 다섯 개의 텍스트 필드 사이를 이동할 수 있는 예제입니다.
import androidx.compose.ui.window.application
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.OutlinedTextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
fun main() = application {
Window(
state = WindowState(size = DpSize(350.dp, 500.dp)),
onCloseRequest = ::exitApplication
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(50.dp)
) {
for (x in 1..5) {
val text = remember { mutableStateOf("") }
OutlinedTextField(
value = text.value,
singleLine = true,
onValueChange = { text.value = it }
)
Spacer(modifier = Modifier.height(20.dp))
}
}
}
}
}
또한 focus가 불가능한 Composable를 focusable하게 만들고 탭 순서를 커스텀하여 해당 Composable에 focus를 조절할 수 있습니다.
다음은 각각의 탭에 대해서 설명된 문서를 번역 및 정리할 예정입니다.