当前位置: 首页 > article >正文

Compose Multiplatform 实现自定义的系统托盘,解决托盘乱码问题

Compose Multiplatform是 JetBrains 开发的声明式 UI 框架,可让您为 Android、iOS、桌面和 Web 开发共享 UI。将 Compose Multiplatform 集成到您的 Kotlin Multiplatform 项目中,即可更快地交付您的应用和功能,而无需维护多个 UI 实现。

在(2025.06.05) Compose Multiplatform 中对于 Desktop 的开发,如果使用了托盘,会发现托盘中的中文竟然是乱码。为了解决这个问题,只能重新实现一个系统托盘,因此该托盘具备了以下特性。

  • 解决中文乱码
  • 更多的Swing 组件可以被放到托盘
  • 允许你监听单击事件,并获取单击位置。方便你绘制类似于 Toolbox 的窗体
  • 乱序的菜单项,除非你手动指定菜单顺序
package io.github.zimoyin.xianyukefuimport androidx.compose.runtime.*
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.toAwtImage
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.window.Notification
import androidx.compose.ui.window.TrayState
import androidx.compose.ui.window.rememberTrayState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseListener
import java.util.*
import javax.swing.*
import javax.swing.border.Border/*** 托盘窗口* 使用 JDialog 作为 JPopupMenu 载体。实现托盘菜单。* 允许在里面设置复杂菜单项,并解决了中文乱码问题。* 使用方式与 Tray() 接近** @param icon 图标* @param tooltip 提示* @param state 控制托盘和显示通知的状态* @param onClick 菜单被鼠标单击时触发,无论是左键还是右键* @param onAction 菜单被双击时触发* @param onVisible 菜单显示时触发* @param onInvisible 菜单隐藏时触发* @param isSort 是否对菜单进行排序,默认为 false* @param setLookAndFeel 设置Swing 的皮肤。如果使用系统的皮肤请使用 UIManager.getSystemLookAndFeelClassName() 获取值* @param content 菜单内容*/
@Composable
fun TrayWindow(icon: Painter,tooltip: String? = null,state: TrayState = rememberTrayState(),onClick: (TrayClickEvent) -> Unit = {},isSort: Boolean = false,onAction: () -> Unit = {},onVisible: () -> Unit = {},onInvisible: () -> Unit = {},style: ComponentStyle = ComponentStyle(),setLookAndFeel: String? = null,content: @Composable MenuScope.() -> Unit = {},
) {setLookAndFeel?.let { UIManager.setLookAndFeel(it) }val awtIcon = remember(icon) {icon.toAwtImage(GlobalDensity, GlobalLayoutDirection, iconSize)}val menuWindow = remember { JDialog() }.apply {isUndecorated = true//作为菜单载体不需要存在可以视的窗体setSize(0, 0)}val coroutineScopeR = rememberCoroutineScope()val onClickR by rememberUpdatedState(onClick)val onActionR by rememberUpdatedState(onAction)val contentR by rememberUpdatedState(content)val onVisibleR by rememberUpdatedState(onVisible)val onInvisibleR by rememberUpdatedState(onInvisible)//创建JPopupMenuval menu: JPopupMenu = remember {TrayMenu(onVisible = {menuWindow.isVisible = trueonVisibleR()},onInvisible = {menuWindow.isVisible = falseonInvisibleR()})}.apply {style.setStyle2(this)}val menuScopeR by rememberUpdatedState(MenuScope(menu, isSort = isSort))//重绘菜单menu.removeAll()contentR(menuScopeR)val menuSizeR = calculationMenuSize(menu)val trayIcon = remember {TrayIcon(awtIcon).apply {isImageAutoSize = true//给托盘图标添加鼠标监听addMouseListener(object : MouseAdapter() {override fun mouseReleased(e: MouseEvent) {val pointer = MouseInfo.getPointerInfo().locationonClickR(TrayClickEvent(e.x,e.y,pointer.x,pointer.y,ButtonType.createButtonType(e.button),e.isPopupTrigger,e))if (e.button == 3 && e.isPopupTrigger) {openMenu(pointer, menuWindow, menu, menuSizeR)}}})addActionListener {onActionR()}}}.apply {if (toolTip != tooltip) toolTip = tooltip}DisposableEffect(Unit) {// 将托盘图标添加到系统的托盘实例中SystemTray.getSystemTray().add(trayIcon)state.notificationFlow.onEach(trayIcon::displayMessage).launchIn(coroutineScopeR)onDispose {menuWindow.dispose()SystemTray.getSystemTray().remove(trayIcon)}}
}private fun TrayIcon.displayMessage(notification: Notification) {val messageType = when (notification.type) {Notification.Type.None -> TrayIcon.MessageType.NONENotification.Type.Info -> TrayIcon.MessageType.INFONotification.Type.Warning -> TrayIcon.MessageType.WARNINGNotification.Type.Error -> TrayIcon.MessageType.ERROR}displayMessage(notification.title, notification.message, messageType)
}/*** 弹出菜单* @param menuWindow 菜单绑定的容器* @param menu 菜单*/
private fun openMenu(pointer: Point, menuWindow: JDialog, menu: JPopupMenu, menuSize: Dimension) {val x = pointer.xval y = pointer.y//右键点击弹出JPopupMenu绑定的载体以及JPopupMenumenuWindow.setLocation(x, y)menuWindow.isVisible = truemenu.show(menuWindow, 3, 0 - (menuSize.height + 3))
}/*** 点击事件*/
data class TrayClickEvent(val x: Int,val y: Int,val mouseX: Int,val mouseY: Int,val buttonType: ButtonType,val isPopupTrigger: Boolean,val awtEvent: MouseEvent,
)/*** 按钮类型*/
enum class ButtonType {LEFT,RIGHT,UNDEFINED;companion object {fun createButtonType(button: Int): ButtonType = when (button) {1 -> LEFT3 -> RIGHTelse -> UNDEFINED}}
}/*** 计算菜单的尺寸*/
fun calculationMenuSize(menu: JPopupMenu): Dimension {var menuHeight = 0var menuWidth = 0for (component in menu.components) {if (component is JMenuItem && component.isVisible) {val size = component.getPreferredSize()menuHeight += size.heightmenuWidth += size.width}}return Dimension(menuWidth, menuHeight)
}/*** 菜单域,用于添加控件*/
class MenuScope(val menu: JPopupMenu, val menuItem: JMenu? = null, var isSort: Boolean = false) {private fun Painter.toAwtImageIcon(): ImageIcon {return ImageIcon(toAwtImage(GlobalDensity, GlobalLayoutDirection))}companion object {private val orderMap = HashMap<Int, Int>()private val COM = HashMap<Int, HashSet<Order>>()}data class Order(val key: UUID,var order: Int,) {override fun equals(other: Any?): Boolean {if (this === other) return trueif (other !is Order) return falseif (key != other.key) return falsereturn true}override fun hashCode(): Int {return key.hashCode()}}fun getItemCount(): Int {return menuItem?.itemCount ?: menu.componentCount}private fun getOrderKey(): Int {return menuItem?.hashCode() ?: menu.hashCode()}@Composableprivate fun rememberOrder(): Int {if (!isSort) return -1val orderKey = getOrderKey()val key by remember { mutableStateOf(UUID.randomUUID()) }val list = COM.getOrPut(orderKey) {hashSetOf()}var order = list.lastOrNull { it.key == key }if (order == null) {order = Order(key, list.size)if (order.order <= getItemCount()) list.add(order)else order.order -= 1}//        println("${if (menuItem != null) "menuItem" else "menu"} : $order itemCount: ${getItemCount()}   key: $key")return order.order}private fun removeOrder(order: Int) {if (order == -1) returnval orderKey = getOrderKey()val list = COM[orderKey] ?: returnif (list.isEmpty()) returnlist.removeIf {it.order == order}val result = list.filter {it.order >= order}.map {Order(it.key, it.order - 1)}result.forEach { rus ->list.removeIf {it.key == rus.key}}list.addAll(result)}/*** 通用菜单项** @param text 菜单项文本内容,默认为 null* @param icon 菜单项图标,默认为 null* @param enabled 是否启用,默认为 true* @param mnemonic 快捷键字符,默认为 null* @param style 组件样式,默认为 [ComponentStyle]* @param orderIndex 菜单项排序索引,默认为 -1* @param onClick 点击菜单项时的回调函数*/@Composablefun Item(text: String? = null,icon: Painter? = null,enabled: Boolean = true,mnemonic: Char? = null,style: ComponentStyle = ComponentStyle(),orderIndex: Int = -1,onClick: () -> Unit = {},) {val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}fun createItem() = JMenuItem(text, icon?.toAwtImageIcon()).apply {addActionListener {if (isEnabled) onClick()}if (mnemonic != null) this.accelerator = KeyStroke.getKeyStroke(mnemonic.uppercaseChar())isEnabled = enabledstyle.setStyle(this)
//            println("text: $text  order: $order sort:$isSort")menuItem?.add(this, order) ?: menu.add(this, order)}var item by remember { mutableStateOf(createItem()) }LaunchedEffect(icon, text, enabled, onClick, style.id(), mnemonic, order) {menuItem?.remove(item) ?: menu.remove(item)item = createItem()}if (menuItem != null) {menuItem.remove(item)menuItem.add(item, order)}DisposableEffect(Unit) {onDispose {menuItem?.remove(item) ?: menu.remove(item)removeOrder(order)}}}/*** 文字标签** @param text 标签文本内容* @param enabled 是否启用,默认为 true* @param mnemonic 快捷键字符,默认为 null* @param style 组件样式,默认为 [ComponentStyle]* @param orderIndex 标签排序索引,默认为 -1* @param onClick 点击标签时的回调函数*/@Composablefun Label(text: String,enabled: Boolean = true,mnemonic: Char? = null,style: ComponentStyle = ComponentStyle(),orderIndex: Int = -1,onClick: () -> Unit = {},) {Item(text, enabled = enabled, mnemonic = mnemonic, style = style, onClick = onClick, orderIndex = orderIndex)}/*** 分割线* @param orderIndex 排序序号,-1表示默认排序*/@Composablefun Separator(orderIndex: Int = -1) {check(menuItem == null) { "Separator only support menu" }val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}val jSeparator = remember {JSeparator(SwingConstants.HORIZONTAL).apply {menu.add(this, order)}}DisposableEffect(Unit) {onDispose {menu.remove(jSeparator)removeOrder(order)}}}/*** 垂直分割线* @param orderIndex 排序序号,-1表示默认排序*/@Composablefun VerticalSeparator(orderIndex: Int = -1) {check(menuItem == null) { "VerticalSeparator only support menu" }val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}val jSeparator = remember {JSeparator(SwingConstants.VERTICAL).apply {menu.add(this, order)removeOrder(order)}}DisposableEffect(Unit) {onDispose {menu.remove(jSeparator)removeOrder(order)}}}/*** 复选框菜单项** @param text 菜单项文本内容,默认为 null* @param icon 菜单项图标,默认为 null* @param selected 是否选中,默认为 false* @param enabled 是否启用,默认为 true* @param mnemonic 快捷键字符,默认为 null* @param style 组件样式,默认为 [ComponentStyle]* @param orderIndex 菜单项排序索引,默认为 -1* @param onCheckedChange 复选框状态变化时的回调函数*/@Composablefun CheckboxItem(text: String? = null,icon: Painter? = null,selected: Boolean = false,enabled: Boolean = true,mnemonic: Char? = null,style: ComponentStyle = ComponentStyle(),orderIndex: Int = -1,onCheckedChange: (Boolean) -> Unit = {},) {val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}fun createItem() = JCheckBoxMenuItem(text, icon?.toAwtImageIcon(), selected).apply {addActionListener {onCheckedChange(isSelected)}if (mnemonic != null) this.accelerator = KeyStroke.getKeyStroke(mnemonic.uppercaseChar())isEnabled = enabledstyle.setStyle(this)menuItem?.add(this, order) ?: menu.add(this, order)}var item by remember { mutableStateOf(createItem()) }LaunchedEffect(icon, text, enabled, selected, style.id(), mnemonic, onCheckedChange, orderIndex) {menuItem?.remove(item) ?: menu.remove(item)item = createItem()}DisposableEffect(Unit) {onDispose {menuItem?.remove(item) ?: menu.remove(item)removeOrder(order)}}}/*** 单选按钮菜单项** @param text 菜单项文本内容,默认为 null* @param icon 菜单项图标,默认为 null* @param selected 是否选中,默认为 false* @param enabled 是否启用,默认为 true* @param style 组件样式,默认为 [ComponentStyle]* @param orderIndex 菜单项排序索引,默认为 -1* @param onCheckedChange 单选按钮状态变化时的回调函数**/@Composablefun RadioButtonItem(text: String? = null,icon: Painter? = null,selected: Boolean = false,enabled: Boolean = true,style: ComponentStyle = ComponentStyle(),orderIndex: Int = -1,onCheckedChange: (Boolean) -> Unit = {},) {val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}fun createItem() = JRadioButton(text, icon?.toAwtImageIcon(), selected).apply {addActionListener {onCheckedChange(isSelected)}isEnabled = enabledstyle.setStyle(this)menuItem?.add(this, order) ?: menu.add(this, order)}var item by remember {mutableStateOf(createItem())}LaunchedEffect(icon, text, enabled, selected, style.id(), onCheckedChange, orderIndex) {menuItem?.remove(item) ?: menu.remove(item)item = createItem()}DisposableEffect(Unit) {onDispose {menuItem?.remove(item) ?: menu.remove(item)removeOrder(order)}}}/*** 子菜单** @param text 子菜单名称* @param visible 是否可见,默认为 true* @param enabled 是否启用,默认为 true* @param mnemonic 快捷键字符,默认为 null* @param style 组件样式,默认为 [ComponentStyle]* @param orderIndex 菜单项排序索引,默认为 -1* @param content 菜单内容的组合构建器**/@Composablefun Menu(text: String = "子菜单",visible: Boolean = true,enabled: Boolean = true,mnemonic: Char? = null,style: ComponentStyle = ComponentStyle(),orderIndex: Int = -1,content: @Composable MenuScope.() -> Unit,) {val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}fun createItem() = JMenu(text).apply {isVisible = visibleisEnabled = enabledif (mnemonic != null) this.accelerator = KeyStroke.getKeyStroke(mnemonic.uppercaseChar())style.setStyle(this)menuItem?.add(this, order) ?: menu.add(this, order)}var item by remember {mutableStateOf(createItem())}MenuScope(menu, item, isSort = isSort).apply {content(this)}LaunchedEffect(text, enabled, visible, style.id(), content, mnemonic, orderIndex) {menuItem?.remove(item) ?: menu.remove(item)item = createItem()}DisposableEffect(Unit) {onDispose {menuItem?.remove(item) ?: menu.remove(item)removeOrder(order)}}}@Deprecated("可能存在bug")@Composablefun Component(orderIndex: Int = -1,content: @Composable MenuScope.() -> Component,) {val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}val item by rememberUpdatedState(content())DisposableEffect(order, content) {menuItem?.add(item, order) ?: menu.add(item, order)onDispose {menuItem?.remove(item) ?: menu.remove(item)removeOrder(order)}}}@Deprecated("可能存在bug")@Composablefun Component(orderIndex: Int = -1,component: Component,) {val scope = rememberCoroutineScope()val order = if (orderIndex >= 0) {scope.launch { withContext(Dispatchers.IO) { initCustomSorting() } }if (isSort) orderIndex else -1} else {rememberOrder()}val item = remember { component }menuItem?.add(item, order) ?: menu.add(item, order)DisposableEffect(orderIndex, component) {onDispose {menuItem?.remove(item) ?: menu.remove(item)removeOrder(order)}}}/*** 初始化菜单排序*/private fun initCustomSorting() {if (!isSort) returnif (menu.components.count { !it.isVisible } <= 9) {for (i in 0..10) {menu.add(JMenuItem("Null").apply {isVisible = false})}}if (menuItem != null) {var count = 0var composeCount = 0for (i in 0 until menuItem.itemCount) {if (!menuItem.getItem(i).isVisible) {count++} else {composeCount++}}if (count <= 9) {for (i in 0..10) {menuItem.add(JMenuItem("Null").apply {isVisible = false})}}}}}/*** 菜单主体*/
internal class TrayMenu(val onInvisible: () -> Unit = {},val onVisible: () -> Unit = {},
) : JPopupMenu() {init {setSize(100, 30)}override fun firePopupMenuWillBecomeInvisible() {onInvisible()}override fun firePopupMenuWillBecomeVisible() {super.firePopupMenuWillBecomeVisible()onVisible()}
}/*** 组件样式*/
data class ComponentStyle(/*** 组件字体*/val font: Font? = null,/*** 组件背景色*/val background: androidx.compose.ui.graphics.Color? = null,/*** 组件文字颜色*/val foreground: androidx.compose.ui.graphics.Color? = null,/*** 组件边框*/val border: Border? = null,/*** 组件边距*/val margin: Insets? = null,/*** 组件位置*/val bounds: Rectangle? = null,/*** 组件位置*/val location: Point? = null,/*** 组件大小*/val size: Dimension? = null,
) {private var color: Color? = background?.toAwtColor()/*** 鼠标进入事件*/val onMouseEnter: (MouseEvent) -> Unit = {color = it.component.backgroundit.component.background = color}/*** 鼠标离开事件*/val onMouseExit: (MouseEvent) -> Unit = {it.component.background = color ?: Color.white}/*** 鼠标点击事件*/val onMouseClick: (MouseEvent) -> Unit = {}/*** 鼠标按下事件*/val onMousePressed: (MouseEvent) -> Unit = {}/*** 鼠标释放事件*/val onMouseReleased: (MouseEvent) -> Unit = {}/*** 计算组件样式的唯一标识,注意部分样式未能计算到*/fun id(): Int {val s = font?.hashCode().toString() +background?.toArgb().toString() +foreground?.toArgb().toString() +margin?.top.toString() + margin?.left?.toString() + margin?.bottom?.toString() + margin?.right?.toString() +bounds?.x?.toString() + bounds?.y.toString() + bounds?.height.toString() + bounds?.width.toString() +location?.x.toString() + location?.y.toString() +size?.height.toString() + size?.width.toString()return s.hashCode()}fun setStyle(component: AbstractButton) {val style = thisif (font != null) component.font = fontif (foreground != null) component.foreground = foreground.toAwtColor()if (background != null) component.background = background.toAwtColor()if (border != null) component.border = borderif (size != null) component.size = this.sizeif (location != null) component.location = this.locationif (margin != null) component.margin = marginif (bounds != null) component.bounds = boundscomponent.addMouseListener(object : MouseListener {override fun mouseClicked(e: MouseEvent) {style.onMouseClick(e)}override fun mousePressed(e: MouseEvent) {style.onMousePressed(e)}override fun mouseReleased(e: MouseEvent) {style.onMouseReleased(e)}override fun mouseEntered(e: MouseEvent) {style.onMouseEnter(e)}override fun mouseExited(e: MouseEvent) {style.onMouseExit(e)}})}fun setStyle2(component: JComponent) {val style = thisif (font != null) component.font = fontif (foreground != null) component.foreground = foreground.toAwtColor()if (background != null) component.background = background.toAwtColor()if (border != null) component.border = borderif (size != null) component.size = this.sizeif (location != null) component.location = this.locationif (bounds != null) component.bounds = boundscomponent.addMouseListener(object : MouseListener {override fun mouseClicked(e: MouseEvent) {style.onMouseClick(e)}override fun mousePressed(e: MouseEvent) {style.onMousePressed(e)}override fun mouseReleased(e: MouseEvent) {style.onMouseReleased(e)}override fun mouseEntered(e: MouseEvent) {style.onMouseEnter(e)}override fun mouseExited(e: MouseEvent) {style.onMouseExit(e)}})}
}// 辅助函数
// 来自于 Compose 内部的函数,不确定是否会引发问题
internal val GlobalDensityget() = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration.density
private val GraphicsConfiguration.density: Densityget() = Density(defaultTransform.scaleX.toFloat(),fontScale = 1f)internal val GlobalLayoutDirection get() = Locale.getDefault().layoutDirection
internal val Locale.layoutDirection: LayoutDirectionget() = ComponentOrientation.getOrientation(this).layoutDirection
internal val ComponentOrientation.layoutDirection: LayoutDirectionget() = when {isLeftToRight -> LayoutDirection.LtrisHorizontal -> LayoutDirection.Rtlelse -> LayoutDirection.Ltr}internal val iconSize = when (DesktopPlatform.Current) {// https://doc.qt.io/qt-5/qtwidgets-desktop-systray-example.html (search 22x22)DesktopPlatform.Linux -> Size(22f, 22f)// https://doc.qt.io/qt-5/qtwidgets-desktop-systray-example.html (search 16x16)DesktopPlatform.Windows -> Size(16f, 16f)// https://medium.com/@acwrightdesign/creating-a-macos-menu-bar-application-using-swiftui-54572a5d5f87DesktopPlatform.MacOS -> Size(22f, 22f)DesktopPlatform.Unknown -> Size(32f, 32f)
}enum class DesktopPlatform {Linux,Windows,MacOS,Unknown;companion object {/*** Identify OS on which the application is currently running.*/val Current: DesktopPlatform by lazy {val name = System.getProperty("os.name")when {name?.startsWith("Linux") == true -> Linuxname?.startsWith("Win") == true -> Windowsname == "Mac OS X" -> MacOSelse -> Unknown}}}
}private fun androidx.compose.ui.graphics.Color.toAwtColor(): Color = Color(this.red, this.green, this.blue, this.alpha)

使用示例

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
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.Modifier
import androidx.compose.ui.geometry.Offset
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.WindowState
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberNotification
import androidx.compose.ui.window.rememberTrayState
import androidx.compose.ui.window.rememberWindowState
import io.github.zimoyin.xianyukefu.ButtonType
import io.github.zimoyin.xianyukefu.TrayWindow
import javax.swing.JButtonfun main() = application {var count by remember { mutableStateOf(0) }val WindowState = rememberWindowState()val isWindowShow = remember { mutableStateOf(true) }val trayState = rememberTrayState()val notification = rememberNotification("Notification", "Message from MyApp!")TrayWindow(state = trayState,icon = TrayIcon,onAction = {if (!isWindowShow.value) isWindowShow.value = trueWindowState.isMinimized = false},onClick = {if (!isWindowShow.value) isWindowShow.value = trueWindowState.isMinimized = false}) {Box {Text("23123")}Item("增加值") {count++}Item("发送通知") {trayState.sendNotification(notification)}Item("退出") {exitApplication()}// Item// Label// Separator// VerticalSeparator// CheckboxItem// RadioButtonItem// Menu// Component // 用于添加 JWT 的组件}Window(onCloseRequest = {isWindowShow.value},icon = MyAppIcon,state = WindowState) {// 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))}
}
http://www.lryc.cn/news/2401498.html

相关文章:

  • 风控研发大数据学习路线
  • 【设计模式】门面/外观模式
  • spring的webclient与vertx的webclient的比较
  • 贪心算法应用:埃及分数问题详解
  • 高效集成AI能力:使用开放API打造问答系统,不用训练模型,也能做出懂知识的AI
  • Qt 仪表盘源码分享
  • Python数据可视化科技图表绘制系列教程(四)
  • RPM 数据库修复
  • R语言基础知识总结(超详细整理)
  • 深入理解系统:UML类图
  • C# 中的 IRecipient
  • 大模型RNN
  • Python环境搭建竞赛技术文章大纲
  • Redisson - 实现延迟队列
  • 软件工程的定义与发展历程
  • 艾利特协作机器人:重新定义工业涂胶场景的精度革命
  • 第十三节:第五部分:集合框架:集合嵌套
  • Java设计模式之观察者模式详解
  • freeRTOS 消息队列之一个事件添加到消息队列超时怎么处理
  • 十八、【用户认证篇】安全第一步:基于 JWT 的前后端分离认证方案
  • RabbitMQ 开机启动配置教程
  • Authpf(OpenBSD)认证防火墙到ssh连接到SSH端口转发技术栈 与渗透网络安全的关联 (RED Team Technique )
  • 组合与排列
  • 神经网络-Day45
  • 【西门子杯工业嵌入式-1-基本环境与空白模板】
  • Apache Druid
  • 使用深蓝词库软件导入自定义的词库到微软拼音输入法
  • Docker快速部署AnythingLLM全攻略
  • 使用Node.js分片上传大文件到阿里云OSS
  • 高性能分布式消息队列系统(四)