Compose Multiplatform для настольных компьютеров предоставляет различные возможности для управления окнами. Вы можете скрывать окна в системном трее, делать их перетаскиваемыми, менять их размер и положение и многое другое.
Чтобы создать обычное окно, можно использовать функцию Window(). Чтобы поместить её в область видимости composable-функции, используйте Window() в точке входа application:
import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { Window(onCloseRequest = ::exitApplication) { // Content of the window } }
Как composable-функция, Window() позволяет декларативно изменять её свойства. Например, можно открыть окно с одним заголовком, а затем изменить его:
Открывать и закрывать окна можно также с помощью простых условий if. В следующем примере кода окно приложения автоматически закрывается после выполнения задачи:
import androidx.compose.material.Text import androidx.compose.runtime.LaunchedEffect 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 import kotlinx.coroutines.delay fun main() = application { var isPerformingTask by remember { mutableStateOf(true) } LaunchedEffect(Unit) { // Do some heavy lifting delay(2000) isPerformingTask = false } if (isPerformingTask) { Window( onCloseRequest = ::exitApplication, title = "Window 1" ) { Text("Performing some tasks. Please wait!") } } else { Window( onCloseRequest = ::exitApplication, title = "Window 2" ) { Text("Hello, World!") } } }
Запрос подтверждения при закрытии
Если при выходе из приложения нужно выполнить пользовательскую логику, например показать диалоговое окно, можно переопределить действие закрытия с помощью обратного вызова onCloseRequest. В следующем примере вместо императивного подхода (window.close()) используется декларативный: окно закрывается в ответ на изменение состояния (isOpen = false).
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.DialogWindow import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { var isOpen by remember { mutableStateOf(true) } var isAskingToClose by remember { mutableStateOf(false) } if (isOpen) { Window( onCloseRequest = { isAskingToClose = true }, title = "Important document" ) { if (isAskingToClose) { DialogWindow( onCloseRequest = { isAskingToClose = false }, title = "Close without saving?" ) { Button( onClick = { isOpen = false } ) { Text("Yes") } } } } } }
Создание одноконного приложения
Для простого приложения с одним окном верхнего уровня не нужна полноценная точка входа application с composable-функцией Window() — функция singleWindowApplication() объединяет их в одном вызове:
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
// Content of the window
}
Если требуется несколько окон верхнего уровня, пользовательская логика закрытия или изменение атрибутов окна во время выполнения, используйте composable-функцию Window() в точке входа application.
Управление состоянием окна
Класс WindowState хранит сведения о размещении окна, его текущем положении и размере. Атрибут размещения позволяет указать, как окно располагается на экране: в обычном режиме, развёрнутым/свёрнутым или в полноэкранном режиме. Любое изменение состояния запускает автоматическую рекомпозицию. Чтобы изменить состояние окна, используйте обратные вызовы или отслеживайте его в composable-функциях:
Чтобы задать размер окна в соответствии с его содержимым, не указывая размеры заранее, задайте для одного или обоих измерений окна значение Dp.Unspecified. Compose Multiplatform автоматически подберёт исходный размер окна по его содержимому:
Чтобы реагировать на изменения состояния и передавать значение в часть приложения, не являющуюся composable-функцией (например, для записи в базу данных), можно использовать функцию snapshotFlow(). Эта функция фиксирует текущее значение состояния composable-функции.
Для управления несколькими окнами можно создать отдельный класс для состояния приложения и открывать или закрывать окна в ответ на изменения mutableStateListOf:
import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { val applicationState = remember { MyApplicationState() } for (window in applicationState.windows) { key(window) { MyWindow(window) } } } @Composable private fun MyWindow( state: MyWindowState ) = Window(onCloseRequest = state::close, title = state.title) { MenuBar { Menu("File") { Item("New window", onClick = state.openNewWindow) Item("Exit", onClick = state.exit) } } } private class MyApplicationState { val windows = mutableStateListOf<MyWindowState>() init { windows += MyWindowState("Initial window") } fun openNewWindow() { windows += MyWindowState("Window ${windows.size}") } fun exit() { windows.clear() } private fun MyWindowState( title: String ) = MyWindowState( title, openNewWindow = ::openNewWindow, exit = ::exit, windows::remove ) } private class MyWindowState( val title: String, val openNewWindow: () -> Unit, val exit: () -> Unit, private val close: (MyWindowState) -> Unit ) { fun close() = close(this) }
С помощью composable-функции DialogWindow() можно отображать отдельное окно уровня ОС с собственной строкой заголовка. Это удобно для подтверждений, выбора файлов или любого взаимодействия, которое пользователь должен завершить, прежде чем продолжить работу.
С помощью экспериментального параметра modalityType можно управлять тем, блокирует ли диалоговое окно взаимодействие с другими окнами. Задайте одно из значений DialogModalityType:
Modeless не блокирует другие окна.
DocumentModal блокирует родительское окно верхнего уровня и все связанные с ним окна, кроме дочерних окон диалогового окна.
ApplicationModal блокирует все остальные окна в том же приложении.
В следующем примере кода обычное окно объединено с диалоговым окном ApplicationModal:
По умолчанию закрытие окна завершает приложение. Чтобы вместо этого скрыть окно в системный трей или строку меню, можно перехватить onCloseRequest и изменить состояние видимости окна.
В следующем примере при закрытии окна значение isVisible устанавливается в false, в результате чего окно скрывается и отображается значок в системном трее. Нажатие на значок восстанавливает окно.
import androidx.compose.material.Text import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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( // Hides the window instead of closing the app 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, tooltip = "Counter", onAction = { isVisible = true }, menu = { Item("Exit", onClick = ::exitApplication) }, ) } } object TrayIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color(0xFFFFA500)) } }
Создание перетаскиваемых областей окна
Чтобы добавить пользовательскую перетаскиваемую строку заголовка в окно без оформления или сделать перетаскиваемым всё окно, можно использовать composable-функцию WindowDraggableArea():
WindowDraggableArea() можно использовать только внутри composable-функций singleWindowApplication(), Window() и DialogWindow(). Чтобы вызвать её в другой composable-функции, используйте WindowScope в качестве области-получателя:
Чтобы создать прозрачное окно, передайте функции Window() два параметра: transparent=true и undecorated=true. Окно должно быть без оформления, поскольку оформить прозрачное окно невозможно.
В следующем примере кода показано, как объединить composable-функции для создания прозрачного окна с закруглёнными углами:
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Surface import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.material.Text import androidx.compose.runtime.* fun main() = application { var isOpen by remember { mutableStateOf(true) } if (isOpen) { Window( onCloseRequest = { isOpen = false }, title = "Transparent Window Example", transparent = true, // Transparent window must be undecorated undecorated = true, ) { Surface( modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)), color = Color.Transparent, // Window with rounded corners shape = RoundedCornerShape(20.dp) ) { Text("Hello World!", color = Color.White) } } } }
Использование компонентов Swing
Compose Multiplatform для настольных компьютеров использует Swing под капотом, поэтому окно можно создать напрямую с помощью Swing:
Также можно использовать область видимости composable-функции Window(). В следующем примере кода window — это ComposeWindow, созданный внутри Window():
Начиная с Compose Multiplatform 1.12.0, в под-пакете androidx.compose.ui.window.v2 доступны переработанные классы WindowState и DialogState.
API окон и диалоговых окон v2 отделяет запрос состояния от наблюдения за состоянием, фактически применённым диспетчером окон. Оно также открывает возможности для сценариев, которые раньше были недоступны, например для задания размера окна в соответствии с предпочтительным размером его содержимого, при этом содержимое по-прежнему может расширяться (с помощью модификаторов, таких как fillMaxSize()), если окно становится больше. Подробнее см. в разделе Задание размера.
API v2 доступно наряду с существующим API, описанным на остальной части этой страницы, поэтому вы можете переносить отдельные окна в удобном для вас темпе.
Задание состояния и наблюдение за ним
API v2 явно отделяет задание желаемого состояния от наблюдения за фактическим состоянием.
Чтобы задать начальное состояние окна, передайте поставщики в rememberWindowState():
Чтобы запросить изменение состояния после создания окна, вызовите соответствующий метод у WindowState. Точный размер и положение можно задать напрямую:
Запрос применяется асинхронно. Оконная система может изменить запрошенное состояние, а фактическое состояние может измениться позднее, например при перемещении окна или изменении его размера. Наблюдать за фактическим состоянием окна можно с помощью WindowState.screenId и WindowState.bounds:
if (windowState.isInitialized) {
Text("Current screen: ${windowState.screenId}")
Text("Current bounds: ${windowState.bounds}")
}
Та же асинхронная модель доступна для диалоговых окон через DialogState и rememberDialogState().
Выбор экрана
Можно запросить отображение окна на определённом экране, передав initialScreenProvider в rememberWindowState() или вызвав WindowState.requestScreen() позднее. Наблюдать за экраном, на котором фактически размещено окно, можно через WindowState.screenId.
Например, можно запросить размещение окна на экране с доступной шириной не менее 1024.dp, указав экран по умолчанию в качестве запасного варианта:
Чтобы изменить положение окна, передайте initialBoundsProvider в rememberWindowState() или вызовите WindowState.requestBounds() позднее. Наблюдать за фактическими границами окна можно через WindowState.bounds.
API v2 использует WindowPositionProvider для получения информации об экране и геометрии родительского окна.
Для стандартного размещения можно использовать встроенные свойства:
Default применяет стандартное поведение каскадного размещения операционной системы.
Current сохраняет текущее положение окна.
CenteredOnScreen размещает окно по центру экрана.
CenteredInParentWindow размещает окно по центру родительского окна.
Для более гибкой настройки используйте функции-поставщики положения:
Absolute() размещает начальный угол окна по заданным координатам x и y.
AlignedToScreen() выравнивает окно относительно экрана и включает необязательный параметр смещения.
AlignedToParentWindow() привязывает окно к родительскому окну и обычно используется для диалоговых окон.
WindowPositionProvider.AlignedToParentWindow(
// Anchors to the starting corner of the parent window
anchor = Alignment.TopStart,
// Applies alignment relative to the anchor point
alignment = Alignment.Center
)
Задание размера
Размер также входит в границы окна, поэтому он настраивается с помощью того же механизма initialBoundsProvider/WindowState.requestBounds().
API v2 использует WindowSizeProvider для получения информации о размерах экрана и родительского окна, а также для запроса у содержимого окна его естественных размеров.
К часто используемым встроенным параметрам относятся Fixed() для задания определённого размера окна и Default для стандартного размера 800×600 dp.
Для настройки размера пользовательская лямбда WindowSizeProvider() получает доступ к параметрам экрана, а для диалоговых окон — к параметрам родительского окна:
WindowSizeProvider {
val height = parentWindowMetrics!!.bounds.height
DpSize(300.dp, height)
}
API v2 поддерживает один из часто запрашиваемых сценариев: задание размера окна в соответствии с предпочтительным размером его содержимого с возможностью заполнения содержимым всего окна, если пользователь увеличит его. WindowSizeProvider.Unconstrained вычисляет размер содержимого, добавляет отступы окна и ограничивает результат доступным размером экрана. Поскольку размер задаётся независимо от компоновки, содержимое с fillMaxSize() всё равно расширяется, заполняя окно, если пользователь меняет его размер.
Версии компонуемых функций Window() и DialogWindow() для API v2 принимают параметры minSize и maxSize. Если базовый диспетчер окон поддерживает такую возможность, пользователь не сможет изменить размер окна за пределы этих границ: