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.pswp.cn/news/908360.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/908360.shtml
英文地址,請注明出處:http://en.pswp.cn/news/908360.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C++11 Move Constructors and Move Assignment Operators 從入門到精通

文章目錄 一、引言二、基本概念2.1 右值引用&#xff08;Rvalue References&#xff09;2.2 移動語義&#xff08;Move Semantics&#xff09; 三、移動構造函數&#xff08;Move Constructors&#xff09;3.1 定義和語法3.2 示例代碼3.3 使用場景 四、移動賦值運算符&#xff…

Linux配置yum 時間同步服務 關閉防火墻 關閉ESlinux

1、配置yum 1.1、Could not resolve host: mirrorlist.centos.org; 未知的錯誤 https://blog.csdn.net/fansfi/article/details/146369946?fromshareblogdetail&sharetypeblogdetail&sharerId146369946&sharereferPC&sharesourceRockandrollman&sharefr…

使用 uv 工具快速部署并管理 vLLM 推理環境

uv&#xff1a;現代 Python 項目管理的高效助手 uv&#xff1a;Rust 驅動的 Python 包管理新時代 在部署大語言模型&#xff08;LLM&#xff09;推理服務時&#xff0c;vLLM 是一個備受關注的方案&#xff0c;具備高吞吐、低延遲和對 OpenAI API 的良好兼容性。為了提高部署效…

基于sqlite的任務鎖(支持多進程/多線程)

前言 介紹 任務鎖,在多進程服務間控制耗時任務的鎖,確保相同id的耗時任務同時只有一個在執行 依賴 SqliteOp,參考這篇文章 https://blog.csdn.net/weixin_43721000/article/details/137019125 實現方式 utils/taskLock.py import timefrom utils.SqliteOp import Sqli…

html表格轉換為markdown

文章目錄 工具功能亮點1.核心實現解析1. 剪貼板交互2. HTML檢測與提取3. 轉換規則設計 2. 完整代碼 在日常工作中&#xff0c;我們經常遇到需要將網頁表格快速轉換為Markdown格式的場景。無論是文檔編寫、知識整理還是數據遷移&#xff0c;手動轉換既耗時又容易出錯。本文將介紹…

IDEA 中 Undo Commit,Revert Commit,Drop Commit區別

一、Undo Commit 適用情況&#xff1a;代碼修改完了&#xff0c;已經Commit了&#xff0c;但是還未push&#xff0c;然后發現還有地方需要修改&#xff0c;但是又不想增加一個新的Commit記錄。這時可以進行Undo Commit&#xff0c;修改后再重新Commit。如果已經進行了Push&…

【Linux】Linux 進程間通訊-管道

參考博客&#xff1a;https://blog.csdn.net/sjsjnsjnn/article/details/125864580 一、進程間通訊介紹 1.1 進程間通訊的概念 進程通信&#xff08;Interprocess communication&#xff09;&#xff0c;簡稱&#xff1a;IPC 本來進程之間是相互獨立的。但是由于不同的進程…

深度剖析 DeepSeek 開源模型部署與應用:策略、權衡與未來走向

在人工智能技術呈指數級發展的當下&#xff0c;大模型已然成為推動各行業變革的核心驅動力。DeepSeek 開源模型以其卓越的性能和靈活的開源特性&#xff0c;吸引了眾多企業與開發者的目光。如何高效且合理地部署與運用 DeepSeek 模型&#xff0c;成為釋放其巨大潛力的關鍵所在&…

第34次CCF-CSP認證真題解析(目標300分做法)

第34次CCF-CSP認證 矩陣重塑&#xff08;其一&#xff09;AC代碼及解析矩陣重塑&#xff08;其二&#xff09;AC代碼及解析貨物調度AC代碼及解析 矩陣重塑&#xff08;其一&#xff09; 輸入輸出及樣例&#xff1a; AC代碼及解析 1.線性化原矩陣 &#xff1a;由于cin的特性我們…

智能制造數字孿生全要素交付一張網:智造中樞,孿生領航,共建智造生態共同體

在制造業轉型升級的浪潮中&#xff0c;數字孿生技術正成為推動行業變革的核心引擎。從特斯拉通過數字孿生體實現車輛全生命周期優化&#xff0c;到海爾卡奧斯工業互聯網平臺賦能千行百業&#xff0c;數字孿生技術已從概念驗證走向規模化落地。通過構建覆蓋全國的交付網絡&#…

【技術】跨設備鏈路聚合的技術——M-LAG

原創&#xff1a;廈門微思網絡 M-LAG&#xff08;Multichassis Link Aggregation Group&#xff09;提供一種跨設備鏈路聚合的技術。M-LAG通過將兩臺接入交換機以同一個狀態和用戶側設備或服務器進行跨設備的鏈路聚合&#xff0c;把鏈路的可靠性從單板級提升到設備級。同時&…

AI健康小屋+微高壓氧艙:科技如何重構我們的健康防線?

目前&#xff0c;隨著科技和社會的不斷發展&#xff0c;人們的生活水平和方式有了翻天覆地的變化。 從吃飽穿暖到吃好喝好再到健康生活&#xff0c;觀念也在逐漸發生改變。 尤其是在21世紀&#xff0c;大家對健康越來越重視&#xff0c;這就不得不提AI健康小屋和氧艙。 一、A…

Python訓練營---Day44

DAY 44 預訓練模型 知識點回顧&#xff1a; 預訓練的概念常見的分類預訓練模型圖像預訓練模型的發展史預訓練的策略預訓練代碼實戰&#xff1a;resnet18 作業&#xff1a; 嘗試在cifar10對比如下其他的預訓練模型&#xff0c;觀察差異&#xff0c;盡可能和他人選擇的不同嘗試通…

1.文件操作相關的庫

一、filesystem(C17) 和 fstream 1.std::filesystem::path - cppreference.cn - C參考手冊 std::filesystem::path 表示路徑 構造函數&#xff1a; path( string_type&& source, format fmt auto_format ); 可以用string進行構造&#xff0c;也可以用string進行隱式類…

【 java 集合知識 第二篇 】

目錄 1.Map集合 1.1.快速遍歷Map 1.2.HashMap實現原理 1.3.HashMap的擴容機制 1.4.HashMap在多線程下的問題 1.5.解決哈希沖突的方法 1.6.HashMap的put過程 1.7.HashMap的key使用什么類型 1.8.HashMapkey可以為null的原因 1.9.HashMap為什么不采用平衡二叉樹 1.10.Hash…

【Dify 知識庫 API】“根據文本更新文檔” 真的是差異更新嗎?一文講透真實機制!

在使用 Dify 知識庫 API 過程中,很多開發者在調用 /datasets/{dataset_id}/document/update-by-text 接口時,常常會產生一個疑問: ?? 這個接口到底是 “智能差異更新” 還是 “純覆蓋更新”? 網上的資料并不多,很多人根據接口名誤以為是增量更新。今天我結合官方源碼 …

大模型如何革新用戶價值、內容匹配與ROI預估

寫在前面 在數字營銷的戰場上,理解用戶、精準觸達、高效轉化是永恒的追求。傳統方法依賴結構化數據和機器學習模型,在用戶價值評估、人群素材匹配以及策略ROI預估等核心問題上取得了顯著成就。然而,隨著數據維度日益復雜,用戶行為愈發多變,傳統方法也面臨著特征工程繁瑣、…

基于端到端深度學習模型的語音控制人機交互系統

基于端到端深度學習模型的語音控制人機交互系統 摘要 本文設計并實現了一個基于端到端深度學習模型的人機交互系統,通過語音指令控制其他設備的程序運行,并將程序運行結果通過語音合成方式反饋給用戶。系統采用Python語言開發,使用PyTorch框架實現端到端的語音識別(ASR)…

【2025年】解決Burpsuite抓不到https包的問題

環境&#xff1a;windows11 burpsuite:2025.5 在抓取https網站時&#xff0c;burpsuite抓取不到https數據包&#xff0c;只顯示&#xff1a; 解決該問題只需如下三個步驟&#xff1a; 1、瀏覽器中訪問 http://burp 2、下載 CA certificate 證書 3、在設置--隱私與安全--…

Jenkins 工作流程

1. 觸發構建 Jenkins 的工作流程從觸發構建開始。構建可以由以下幾種方式觸發&#xff1a; 代碼提交觸發&#xff1a;通過與版本控制系統&#xff08;如 Git、SVN&#xff09;集成&#xff0c;當代碼倉庫有新的提交時&#xff0c;Jenkins 會自動觸發構建。 定時觸發&#xff…