当前位置: 首页 > 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))}
}

相关文章:

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

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

风控研发大数据学习路线

在如今信息爆炸时代&#xff0c;风控系统离不开大数据技术的支撑&#xff0c;大数据技术可以帮助风控系统跑的更快&#xff0c;算的更准。因此&#xff0c;风控技术研发需要掌握大数据相关技术。然而大数据技术栈内容庞大丰富&#xff0c;风控研发同学很可能会面临以下这些痛点…...

【设计模式】门面/外观模式

MySQL &#xff0c;MyTomcat 的启动 现在有 MySQL &#xff0c;MyTomcat 类&#xff0c;需要依次启动。 public class Application {public static void main(String[] args) {MySQL mySQL new MySQL();mySQL.initDate();mySQL.checkLog();mySQL.unlock();mySQL.listenPort(…...

spring的webclient与vertx的webclient的比较

Spring WebClient 和 Vert.x WebClient 都是基于响应式编程模型的非阻塞 HTTP 客户端&#xff0c;但在设计理念、生态整合和适用场景上存在显著差异。以下是两者的核心比较&#xff1a; &#x1f504; 1. 技术背景与架构 • Spring WebClient ◦ 生态定位&#xff1a;属于 Sp…...

贪心算法应用:埃及分数问题详解

贪心算法与埃及分数问题详解 埃及分数&#xff08;Egyptian Fractions&#xff09;问题是数论中的经典问题&#xff0c;要求将一个真分数表示为互不相同的单位分数之和。本文将用2万字全面解析贪心算法在埃及分数问题中的应用&#xff0c;涵盖数学原理、算法设计、Java实现、优…...

高效集成AI能力:使用开放API打造问答系统,不用训练模型,也能做出懂知识的AI

本文为分享体验感受&#xff0c;非广告。 一、蓝耘平台核心功能与优势 丰富的模型资源库 蓝耘平台提供涵盖自然语言处理、计算机视觉、多模态交互等领域的预训练模型&#xff0c;支持用户直接调用或微调&#xff0c;无需从零开始训练&#xff0c;显著缩短开发周期。 高性能…...

Qt 仪表盘源码分享

Qt 仪表盘源码分享 一、效果展示二、优点三、源码分享四、使用方法 一、效果展示 二、优点 直观性 数据以图表或数字形式展示&#xff0c;一目了然。用户可以快速获取关键信息&#xff0c;无需深入阅读大量文字。 实时性 仪表盘通常支持实时更新&#xff0c;确保数据的时效性。…...

Python数据可视化科技图表绘制系列教程(四)

目录 带基线的棒棒糖图1 带基线的棒棒糖图2 带标记的棒棒糖图 哑铃图1 哑铃图2 包点图1 包点图2 雷达图1 雷达图2 交互式雷达图 【声明】&#xff1a;未经版权人书面许可&#xff0c;任何单位或个人不得以任何形式复制、发行、出租、改编、汇编、传播、展示或利用本博…...

RPM 数据库修复

RPM 数据库修复 1、备份当前数据库&#xff08;重要&#xff01;&#xff09; sudo cp -a /var/lib/rpm /var/lib/rpm.backup此操作保护原始数据&#xff0c;防止修复失败导致数据丢失 2、清除损坏的锁文件 sudo rm -f /var/lib/rpm/__db.*这些锁文件&#xff08;如 __db.00…...

R语言基础知识总结(超详细整理)

一、R语言简介 R是一种用于统计分析、数据可视化和科学计算的开源编程语言和环境。其语法简洁&#xff0c;内置丰富的统计函数和图形函数&#xff0c;广泛应用于数据科学、机器学习和生物统计等领域。 整体知识点目录&#xff1a; R语言基础知识总结 │ ├─ 安装与配置 │ …...

深入理解系统:UML类图

UML类图 类图&#xff08;class diagram&#xff09; 描述系统中的对象类型&#xff0c;以及存在于它们之间的各种静态关系。 正向工程&#xff08;forward engineering&#xff09;在编写代码之前画UML图。 逆向工程&#xff08;reverse engineering&#xff09;从已有代码建…...

C# 中的 IRecipient

IRecipient<TMessage> 是 .NET 中消息传递机制的重要组成部分&#xff0c;特别是在 MVVM (Model-View-ViewModel) 模式中广泛使用。下面我将详细介绍这一机制及其应用。 基本概念 IRecipient<TMessage> 是 .NET Community Toolkit 和 MVVM Toolkit 中定义的一个接…...

大模型RNN

RNN&#xff08;循环神经网络&#xff09;是一种专门处理序列数据的神经网络架构&#xff0c;在自然语言处理&#xff08;NLP&#xff09;、语音识别、时间序列分析等领域有广泛应用。其核心作用是捕捉序列中的时序依赖关系&#xff0c;即当前输出不仅取决于当前输入&#xff0…...

Python环境搭建竞赛技术文章大纲

竞赛背景与意义 介绍Python在数据科学、机器学习等领域的重要性环境搭建对于竞赛项目效率的影响常见竞赛平台对Python环境的特殊要求 基础环境准备 操作系统选择与优化&#xff08;Windows/Linux/macOS&#xff09;Python版本选择&#xff08;3.x推荐版本&#xff09;解释器…...

Redisson - 实现延迟队列

Redisson 延迟队列 Redisson 是基于 Redis 的一款功能强大的 Java 客户端。它提供了诸如分布式锁、限流器、阻塞队列、延迟队列等高可用、高并发组件。 其中&#xff0c;RDelayedQueue 是对 Redis 数据结构的高阶封装&#xff0c;能让你将消息延迟一定时间后再进入消费队列。…...

软件工程的定义与发展历程

文章目录 一、软件工程的定义二、软件工程的发展历程1. 前软件工程时期(1940s-1960s)2. 软件工程诞生(1968)3. 结构化方法时期(1970s)4. 面向对象时期(1980s)5. 现代软件工程(1990s-至今) 三、软件工程的发展趋势 一、软件工程的定义 软件工程是应用系统化、规范化、可量化的方…...

艾利特协作机器人:重新定义工业涂胶场景的精度革命

品牌使命与技术基因 作为全球协作机器人领域成长最快的企业之一&#xff0c;艾利特始终聚焦于解决工业生产中的人机协作痛点。在汽车制造、3C电子、新能源等领域的涂胶工艺场景中&#xff0c;我们通过自主研发的EC系列协作机器人&#xff0c;实现了&#xff1a; 空间利用率&a…...

第十三节:第五部分:集合框架:集合嵌套

集合嵌套案例分析 代码&#xff1a; package com.itheima.day27_Collection_nesting;import java.util.*;/*目标:理解集合的嵌套。 江苏省 "南京市","扬州市","苏州市","无锡市","常州市" 湖北省 "武汉市","…...

Java设计模式之观察者模式详解

一、观察者模式简介 观察者模式&#xff08;Observer Pattern&#xff09;是一种行为型设计模式&#xff0c;它定义了对象之间的一对多依赖关系。当一个对象&#xff08;主题&#xff09;的状态发生改变时&#xff0c;所有依赖于它的对象&#xff08;观察者&#xff09;都会自…...

freeRTOS 消息队列之一个事件添加到消息队列超时怎么处理

一 消息队列的结构框图 xTasksWaitingToSend‌&#xff1a;这个列表存储了所有因为队列已满而等待发送消息的任务。当任务尝试向一个已满的队列发送消息时&#xff0c;该任务会被挂起并加入到xTasksWaitingToSend列表中&#xff0c;直到队列中有空间可用‌&#xff0c; xTasksW…...

十八、【用户认证篇】安全第一步:基于 JWT 的前后端分离认证方案

【用户认证篇】安全第一步:基于 JWT 的前后端分离认证方案 前言什么是 JWT (JSON Web Token)?准备工作第一部分:后端 Django 配置 JWT 认证1. 安装 `djangorestframework-simplejwt`2. 在 `settings.py` 中配置 `djangorestframework-simplejwt`3. 在项目的 `urls.py` 中添加…...

RabbitMQ 开机启动配置教程

RabbitMQ 开机启动配置教程 在本教程中&#xff0c;我们将详细介绍如何配置 RabbitMQ 以实现开机自动启动。此配置适用于手动安装的 RabbitMQ 版本。 环境准备 操作系统&#xff1a;CentOS 7RabbitMQ 版本&#xff1a;3.8.4Erlang 版本&#xff1a;21.3 步骤 1. 安装 Erla…...

Authpf(OpenBSD)认证防火墙到ssh连接到SSH端口转发技术栈 与渗透网络安全的关联 (RED Team Technique )

目录 &#x1f50d; 1. Authpf概述与Shell设置的作用 什么是Authpf&#xff1f; Shell设置为/usr/sbin/authpf的作用与含义 &#x1f6e0;️ 2. Authpf工作原理与防火墙绕过机制 技术栈 工作原理 防火墙绕过机制 Shell关联 &#x1f310; 3. Authpf与SSH认证及服务探测…...

组合与排列

组合与排列主要有两个区别&#xff0c;区别在于是否按次序排列和符号表示不同。 全排列&#xff1a; 从n个不同元素中任取m&#xff08;m≤n&#xff09;个元素&#xff0c;按照一定的顺序排列起来&#xff0c;叫做从n个不同元素中取出m个元素的一个排列。当mn时所有的排列情况…...

神经网络-Day45

目录 一、tensorboard的基本操作1.1 发展历史1.2 tensorboard的原理 二、tensorboard实战2.1 cifar-10 MLP实战2.2 cifar-10 CNN实战 在神经网络训练中&#xff0c;为了帮助理解&#xff0c;借用了很多的组件&#xff0c;比如训练进度条、可视化的loss下降曲线、权重分布图&…...

【西门子杯工业嵌入式-1-基本环境与空白模板】

西门子杯工业嵌入式-1-基本环境与空白模板 项目资料一、软件安装与环境准备1. 安装MDK52. 安装驱动3. 安装GD32F470支持包 二、工程目录结构建议三、使用MDK创建工程流程1. 新建工程2. 添加工程组&#xff08;Group&#xff09;3. 添加源文件 四、编译配置设置&#xff08;Opti…...

Apache Druid

目录 Apache Druid是什么&#xff1f; CVE-2021-25646(Apache Druid代码执行漏洞) Apache Druid是什么&#xff1f; Apache Druid是一个高性能、分布式的数据存储和分析系统。设计用于处理大量实时数据&#xff0c;并进行低延迟的查询。它特别适合用于分析大规模日志、事件数据…...

使用深蓝词库软件导入自定义的词库到微软拼音输入法

我这有一个人员名单&#xff0c;把它看作一个词库&#xff0c;下面我演示一下如何把这个词库导入微软输入法 首先建一个text文件&#xff0c;一行写一个词条 下载深蓝词库 按照我这个配置&#xff0c;点击转换&#xff0c;然后在桌面微软输入法那右键&#xff0c;选择设置 点…...

Docker快速部署AnythingLLM全攻略

Docker版AnythingLLM安装指南 环境准备 确保已安装: Docker Engine 20.10.14+Docker Compose 2.5.0+验证安装: docker --version && docker compose version安装步骤 创建持久化存储目录: mkdir -p ~/anythingllm/database ~/anythingllm/files运行容器(基础配置)…...

使用Node.js分片上传大文件到阿里云OSS

阿里云OSS的分片上传&#xff08;Multipart Upload&#xff09;是一种针对大文件优化的上传方式&#xff0c;其核心流程和关键特性如下&#xff1a; 1. ‌核心流程‌ 分片上传分为三个步骤&#xff1a; 初始化任务‌&#xff1a;调用InitiateMultipartUpload接口创建上传任务…...