Spec-Zone.ru › Kotlin 2

Взаимодействие со Swing

Здесь вы узнаете, как использовать компоненты Swing в приложении Compose Multiplatform и наоборот, каковы ограничения и преимущества такого взаимодействия, а также в каких случаях стоит или не стоит использовать этот подход.

Взаимодействие Compose Multiplatform и Swing призвано помочь вам:

  • Упростить и облегчить перенос приложений Swing на Compose Multiplatform.

  • Расширить возможности приложений Compose Multiplatform с помощью компонентов Swing, если аналоги в Compose отсутствуют.

Во многих случаях эффективнее реализовать отсутствующий компонент непосредственно в Compose Multiplatform (и внести его в сообщество), чем использовать компонент Swing в приложении Compose Multiplatform.

Примеры в этом руководстве — запускаемые настольные приложения. Чтобы попробовать один из них:

  1. Создайте проект Compose Multiplatform с помощью плагина Kotlin Multiplatform для IDE или онлайн-мастера.

  2. Откройте файл точки входа настольного приложения. Например, desktopApp/src/main/kotlin/com/example/my_desktop_app/main.kt.

  3. Замените его содержимое приведённым примером.

Сценарии использования и ограничения взаимодействия со Swing

Компонент Compose Multiplatform в приложении Swing

Первый сценарий — добавление компонента Compose Multiplatform в приложение Swing. Это можно сделать с помощью компонента Swing ComposePanel, который отображает часть приложения на Compose Multiplatform. С точки зрения Swing, ComposePanel — это ещё один компонент Swing, с которым он работает соответствующим образом.

Обратите внимание: все компоненты Compose Multiplatform, в том числе всплывающие окна, подсказки и контекстные меню, отображаются внутри ComposePanel Swing и позиционируются и изменяют размер внутри него. Поэтому рассмотрите возможность заменить эти компоненты реализациями на основе Swing или попробовать две новые экспериментальные функции:

Внеэкранный рендеринг

Позволяет отображать панели Compose непосредственно на компонентах Swing.

Отдельные платформенные представления для всплывающих окон, диалогов и раскрывающихся списков

Всплывающие окна больше не ограничены исходным холстом компонуемого элемента или окном приложения.

Ниже приведены несколько сценариев использования ComposePanel:

  • Встраивание анимированных объектов или целой панели с анимированными объектами в приложение (например, выбора эмотиконов или панели инструментов с анимированными реакциями на события).

  • Реализация интерактивной области отображения, например графики или инфографики, которую проще и удобнее создать с помощью Compose Multiplatform.

  • Интеграция в приложение сложной области отображения, возможно даже анимированной, которую проще создать с помощью Compose Multiplatform.

  • Замена сложных частей пользовательского интерфейса приложения на Swing: Compose Multiplatform предоставляет удобную систему компоновки компонентов, широкий набор встроенных компонентов и возможностей для быстрого создания пользовательских компонентов.

Компонент Swing в приложении Compose Multiplatform

Другой сценарий — когда нужно использовать компонент, который есть в Swing, но не имеет аналога в Compose Multiplatform. Если создание новой реализации с нуля отнимает слишком много времени, попробуйте SwingPanel. Функция SwingPanel служит обёрткой, которая управляет размером, положением и отображением компонента Swing, размещённого поверх компонента Compose Multiplatform.

Обратите внимание: компонент Swing внутри SwingPanel всегда будет находиться поверх компонента Compose Multiplatform, поэтому всё, что расположено под вашим SwingPanel, будет обрезано компонентом Swing. Чтобы избежать обрезки и наложения, попробуйте экспериментальное смешивание при взаимодействии. Если риск некорректного отображения всё ещё сохраняется, можно соответствующим образом переработать интерфейс или отказаться от использования SwingPanel и попробовать реализовать отсутствующий компонент, внеся вклад в развитие технологии.

Ниже приведены сценарии использования SwingPanel:

  • В приложении не нужны всплывающие окна, подсказки или контекстные меню либо, по крайней мере, они не находятся внутри вашего SwingPanel.

  • SwingPanel остаётся в фиксированном положении. В этом случае уменьшается риск сбоев и артефактов при изменении положения компонента Swing. Однако это условие не является обязательным, и его следует проверять в каждом конкретном случае.

Compose Multiplatform и Swing можно сочетать обоими способами, что позволяет гибко проектировать интерфейс. Можно разместить SwingPanel внутри ComposePanel, который, в свою очередь, может находиться внутри другого SwingPanel. Однако перед использованием таких вложенных комбинаций учитывайте возможность артефактов отображения. Пример кода см. в разделе Компоновка с вложенными компонентами SwingPanel и ComposePanel.

Использование Compose Multiplatform в приложении Swing

ComposePanel позволяет создавать интерфейс на Compose Multiplatform в приложении на Swing. Добавьте экземпляр ComposePanel в компоновку Swing и задайте композицию внутри setContent:

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.awt.ComposePanel import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import java.awt.BorderLayout import java.awt.Dimension import javax.swing.JButton import javax.swing.JFrame import javax.swing.SwingUtilities import javax.swing.WindowConstants val northClicks = mutableStateOf(0) val westClicks = mutableStateOf(0) val eastClicks = mutableStateOf(0) fun main() = SwingUtilities.invokeLater { val window = JFrame() // Creates ComposePanel val composePanel = ComposePanel() window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE window.title = "SwingComposeWindow" window.contentPane.add(actionButton("NORTH", action = { northClicks.value++ }), BorderLayout.NORTH) window.contentPane.add(actionButton("WEST", action = { westClicks.value++ }), BorderLayout.WEST) window.contentPane.add(actionButton("EAST", action = { eastClicks.value++ }), BorderLayout.EAST) window.contentPane.add( actionButton( text = "SOUTH/REMOVE COMPOSE", action = { window.contentPane.remove(composePanel) } ), BorderLayout.SOUTH ) // Adds ComposePanel to JFrame window.contentPane.add(composePanel, BorderLayout.CENTER) // Sets the content composePanel.setContent { ComposeContent() } window.setSize(800, 600) window.isVisible = true } fun actionButton(text: String, action: () -> Unit): JButton { val button = JButton(text) button.toolTipText = "Tooltip for $text button." button.preferredSize = Dimension(100, 100) button.addActionListener { action() } return button } @Composable fun ComposeContent() { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Row { Counter("West", westClicks) Spacer(modifier = Modifier.width(25.dp)) Counter("North", northClicks) Spacer(modifier = Modifier.width(25.dp)) Counter("East", eastClicks) } } } @Composable fun Counter(text: String, counter: MutableState<Int>) { Surface( modifier = Modifier.size(130.dp, 130.dp), color = Color(180, 180, 180), shape = RoundedCornerShape(4.dp) ) { Column { Box( modifier = Modifier.height(30.dp).fillMaxWidth(), contentAlignment = Alignment.Center ) { Text(text = "${text}Clicks: ${counter.value}") } Spacer(modifier = Modifier.height(25.dp)) Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Button(onClick = { counter.value++ }) { Text(text = text, color = Color.White) } } } } }
IntegrationWithSwing

Экспериментальный внеэкранный рендеринг

Экспериментальный режим позволяет отображать ComposePanel непосредственно на компонентах Swing. Это предотвращает проблемы с переходным отображением при появлении, скрытии или изменении размера ComposePanel. Кроме того, обеспечивается правильное наложение компонентов Swing и панелей Compose: компонент Swing можно отображать поверх или под ComposePanel. Однако по сравнению с рендерингом Skia по умолчанию это может снизить производительность, причём снижение будет тем заметнее, чем больше размер панели.

Этот режим влияет только на компоненты ComposePanel. На данный момент соответствующих настроек для ComposeWindow или ComposeDialog нет.

Внеэкранный рендеринг имеет статус экспериментальной функции и предназначен только для ознакомительной оценки.

Чтобы включить внеэкранный рендеринг для определённого ComposePanel, передайте значение RenderSettings.SwingGraphics при его создании:

val composePanel = ComposePanel(renderSettings = RenderSettings.SwingGraphics)

Чтобы включить внеэкранный рендеринг по умолчанию для каждого ComposePanel в проекте, используйте флаг функции compose.swing.render.on.graphics:

  • Укажите флаг как аргумент JVM командной строки при запуске:

    -Dcompose.swing.render.on.graphics=true
    
  • Или передайте флаг в качестве аргумента функции System.setProperty() в точке входа:

    fun main() {
        System.setProperty("compose.swing.render.on.graphics", "true")
        ...
    }
    

Экспериментальные отдельные представления для всплывающих окон

Иногда важно, чтобы элементы всплывающих окон, например подсказки и раскрывающиеся меню, не были ограничены исходным холстом компонуемого элемента или окном приложения. Например, когда компонуемое представление не занимает весь экран, но должно открыть диалоговое окно предупреждения.

Создание отдельных представлений или окон для всплывающих окон имеет статус экспериментальной функции. Для её использования требуется явно включить поддержку (см. подробности ниже); функция предназначена только для ознакомительной оценки.

Чтобы создавать отдельные представления или окна для всплывающих окон на настольных платформах, задайте системное свойство compose.layers.type. Поддерживаются следующие значения:

  • WINDOW создаёт компоненты Popup и Dialog в отдельных окнах без оформления.

  • COMPONENT создаёт Popup или Dialog как отдельный компонент Swing в том же окне. Обратите внимание: для этого параметра необходимо включить внеэкранный рендеринг (см. раздел Экспериментальный внеэкранный рендеринг); внеэкранный рендеринг работает только для компонентов ComposePanel, но не для приложений с отдельными окнами.

Обратите внимание: всплывающие окна и диалоговые окна по-прежнему не могут рисовать за пределами собственных границ (например, тень верхнего контейнера).

Ниже приведён пример кода, использующего свойство COMPONENT:

import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.awt.ComposePanel import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import javax.swing.JFrame import javax.swing.JLayeredPane import javax.swing.SwingUtilities import javax.swing.WindowConstants @OptIn(ExperimentalComposeUiApi::class) fun main() = SwingUtilities.invokeLater { System.setProperty("compose.swing.render.on.graphics", "true") System.setProperty("compose.layers.type", "COMPONENT") val window = JFrame() window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE val contentPane = JLayeredPane() contentPane.layout = null val composePanel = ComposePanel() composePanel.setBounds(200, 200, 200, 200) composePanel.setContent { ComposeContent() } // Uses the full window for dialogs composePanel.windowContainer = contentPane contentPane.add(composePanel) window.contentPane.add(contentPane) window.setSize(800, 600) window.isVisible = true } @Composable fun ComposeContent() { Box(Modifier.fillMaxSize().background(Color.Green)) { Dialog(onDismissRequest = {}) { Box(Modifier.size(100.dp).background(Color.Yellow)) } } }

Использование Swing в приложении Compose Multiplatform

SwingPanel позволяет создавать интерфейс Swing в приложении Compose Multiplatform. Используйте параметр factory функции SwingPanel, чтобы создать JPanel Swing:

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication import java.awt.Component import javax.swing.BoxLayout import javax.swing.JButton import javax.swing.JPanel fun main() = singleWindowApplication(title = "SwingPanel") { val counter = remember { mutableStateOf(0) } val inc: () -> Unit = { counter.value++ } val dec: () -> Unit = { counter.value-- } Box( modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp), contentAlignment = Alignment.Center ) { Text("Counter: ${counter.value}") } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(top = 80.dp, bottom = 20.dp) ) { Button("1. Compose Button: increment", inc) Spacer(modifier = Modifier.height(20.dp)) SwingPanel( background = Color.LightGray, modifier = Modifier.size(270.dp, 90.dp), factory = { JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) add(actionButton("1. Swing Button: decrement", dec)) add(actionButton("2. Swing Button: decrement", dec)) add(actionButton("3. Swing Button: decrement", dec)) } } ) Spacer(modifier = Modifier.height(20.dp)) Button("2. Compose Button: increment", inc) } } } @Composable fun Button(text: String = "", action: (() -> Unit)? = null) { Button( modifier = Modifier.size(270.dp, 30.dp), onClick = { action?.invoke() } ) { Text(text) } } fun actionButton( text: String, action: () -> Unit ): JButton { val button = JButton(text) button.alignmentX = Component.CENTER_ALIGNMENT button.addActionListener { action() } return button }
SwingPanel

Обновление компонентов Swing при изменении состояния Compose

Чтобы компонент Swing всегда содержал актуальные данные, передайте callback update: (T) -> Unit, который вызывается при каждом изменении состояния компонуемого элемента или создании его разметки. В следующем примере показано, как обновлять компонент Swing внутри SwingPanel при каждом изменении состояния компонуемого элемента:

import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.width import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.application import androidx.compose.ui.window.Window import androidx.compose.ui.window.rememberWindowState import java.awt.BorderLayout import javax.swing.JPanel import javax.swing.JLabel val swingLabel = JLabel() fun main() = application { Window( onCloseRequest = ::exitApplication, state = rememberWindowState(width = 400.dp, height = 200.dp), title = "SwingLabel" ) { val clicks = remember { mutableStateOf(0) } Column( modifier = Modifier.fillMaxSize().padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { SwingPanel( modifier = Modifier.fillMaxWidth().height(40.dp), factory = { JPanel().apply { add(swingLabel, BorderLayout.CENTER) } }, update = { swingLabel.text = "SwingLabel clicks: ${clicks.value}" } ) Spacer(modifier = Modifier.height(40.dp)) Row ( modifier = Modifier.height(40.dp), verticalAlignment = Alignment.CenterVertically ) { Button(onClick = { clicks.value++ }) { Text(text = "Increment") } Spacer(modifier = Modifier.width(20.dp)) Button(onClick = { clicks.value-- }) { Text(text = "Decrement") } } } } }
SwingLabel

Экспериментальное смешивание при взаимодействии

По умолчанию представление для взаимодействия, реализованное с помощью обёртки SwingPanel, имеет прямоугольную форму и находится на переднем плане — поверх всех компонентов Compose Multiplatform. Чтобы упростить использование всплывающих элементов, мы добавили экспериментальную поддержку смешивания при взаимодействии.

Смешивание при взаимодействии имеет статус экспериментальной функции и предназначено только для ознакомительной оценки.

Чтобы включить эту экспериментальную функцию, задайте системному свойству compose.interop.blending значение true. Свойство необходимо включить до выполнения любого кода Compose в приложении. Для этого задайте его с помощью аргумента JVM командной строки -Dcompose.interop.blending=true или используйте System.setProperty() в точке входа:

fun main() {
    System.setProperty("compose.interop.blending", "true")
    ...
}

Если смешивание при взаимодействии включено, Swing можно использовать в следующих сценариях:

  • Обрезка. Теперь форма не ограничена прямоугольником: модификаторы clip и shadow корректно работают с SwingPanel.

  • Наложение. Поверх SwingPanel можно рисовать любое содержимое Compose Multiplatform и взаимодействовать с ним как обычно.

Подробнее об известных ограничениях см. в описании на GitHub.

Компоновка с вложенными компонентами Swing и Compose Multiplatform

Благодаря взаимодействию можно сочетать Swing и Compose Multiplatform в обоих направлениях: добавлять компоненты Swing в приложение Compose Multiplatform и компоненты Compose Multiplatform в приложение Swing. Поддерживается также сценарий, в котором несколько компонентов вложены друг в друга, а подходы свободно комбинируются.

В следующем примере показано, как добавить SwingPanel в ComposePanel, уже находящийся внутри другого SwingPanel, создав структуру Swing — Compose Multiplatform — Swing:

import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.runtime.* import androidx.compose.ui.awt.* import androidx.compose.ui.* import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.window.* import androidx.compose.ui.unit.* import java.awt.BorderLayout import java.awt.Dimension import java.awt.Insets import javax.swing.* import javax.swing.border.EmptyBorder val Gray = java.awt.Color(64, 64, 64) val DarkGray = java.awt.Color(32, 32, 32) val LightGray = java.awt.Color(210, 210, 210) data class Item( val text: String, val icon: ImageVector, val color: Color, val state: MutableState<Boolean> = mutableStateOf(false) ) val panelItemsList = listOf( Item(text = "Person", icon = Icons.Filled.Person, color = Color(10, 232, 162)), Item(text = "Favorite", icon = Icons.Filled.Favorite, color = Color(150, 232, 150)), Item(text = "Search", icon = Icons.Filled.Search, color = Color(232, 10, 162)), Item(text = "Settings", icon = Icons.Filled.Settings, color = Color(232, 162, 10)), Item(text = "Close", icon = Icons.Filled.Close, color = Color(232, 100, 100)) ) val itemSize = 50.dp fun java.awt.Color.toCompose(): Color { return Color(red, green, blue) } fun main() = application { Window( onCloseRequest = ::exitApplication, state = rememberWindowState(width = 500.dp, height = 500.dp), title = "Layout" ) { Column( modifier = Modifier.fillMaxSize().background(color = Gray.toCompose()).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "Compose Area", color = LightGray.toCompose()) Spacer(modifier = Modifier.height(40.dp)) SwingPanel( background = DarkGray.toCompose(), modifier = Modifier.fillMaxSize(), factory = { ComposePanel().apply { setContent { Box { SwingPanel( modifier = Modifier.fillMaxSize(), factory = { SwingComponent() } ) Box ( modifier = Modifier.align(Alignment.TopStart) .padding(start = 20.dp, top = 80.dp) .background(color = DarkGray.toCompose()) ) { SwingPanel( modifier = Modifier.size(itemSize * panelItemsList.size, itemSize), factory = { ComposePanel().apply { setContent { ComposeOverlay() } } } ) } } } } } ) } } } fun SwingComponent() : JPanel { return JPanel().apply { background = DarkGray border = EmptyBorder(20, 20, 20, 20) layout = BorderLayout() add( JLabel("TextArea Swing Component").apply { foreground = LightGray verticalAlignment = SwingConstants.NORTH horizontalAlignment = SwingConstants.CENTER preferredSize = Dimension(40, 160) }, BorderLayout.NORTH ) add( JTextArea().apply { background = LightGray lineWrap = true wrapStyleWord = true margin = Insets(10, 10, 10, 10) text = "The five boxing wizards jump quickly. " + "Crazy Fredrick bought many very exquisite opal jewels. " + "Pack my box with five dozen liquor jugs.\n" + "Cozy sphinx waves quart jug of bad milk. " + "The jay, pig, fox, zebra and my wolves quack!" }, BorderLayout.CENTER ) } } @Composable fun ComposeOverlay() { Box( modifier = Modifier.fillMaxSize(). background(color = DarkGray.toCompose()), contentAlignment = Alignment.Center ) { Row( modifier = Modifier.background( shape = RoundedCornerShape(4.dp), color = Color.DarkGray.copy(alpha = 0.5f) ) ) { for (item in panelItemsList) { SelectableItem( text = item.text, icon = item.icon, color = item.color, selected = item.state ) } } } } @Composable fun SelectableItem( text: String, icon: ImageVector, color: Color, selected: MutableState<Boolean> ) { Box( modifier = Modifier.size(itemSize) .clickable { selected.value = !selected.value }, contentAlignment = Alignment.Center ) { Column( modifier = Modifier.alpha(if (selected.value) 1.0f else 0.5f), horizontalAlignment = Alignment.CenterHorizontally ) { Icon(modifier = Modifier.size(32.dp), imageVector = icon, contentDescription = null, tint = color) Text(text = text, color = Color.White, fontSize = 10.sp) } } }
Swing layout

Что дальше?

Изучите руководства по другим компонентам для настольных платформ.

21 мая 2026 г.
Тестирование пользовательского интерфейса Compose Multiplatform с помощью JUnitРабота с веб-ресурсами

© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform/compose-desktop-swing-interoperability.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API