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

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

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

  • 解决中文乱码
  • 更多的Swing 组件可以被放到托盘
  • 允许你监听单击事件,并获取单击位置。方便你绘制类似于 Toolbox 的窗体
  • 乱序的菜单项,除非你手动指定菜单顺序
kotlin 复制代码
package io.github.zimoyin.xianyukefu

import 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)

    //创建JPopupMenu
    val menu: JPopupMenu = remember {
        TrayMenu(
            onVisible = {
                menuWindow.isVisible = true
                onVisibleR()
            },
            onInvisible = {
                menuWindow.isVisible = false
                onInvisibleR()
            }
        )
    }.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().location
                    onClickR(
                        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.NONE
        Notification.Type.Info -> TrayIcon.MessageType.INFO
        Notification.Type.Warning -> TrayIcon.MessageType.WARNING
        Notification.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.x
    val y = pointer.y
    //右键点击弹出JPopupMenu绑定的载体以及JPopupMenu
    menuWindow.setLocation(x, y)
    menuWindow.isVisible = true
    menu.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 -> LEFT
            3 -> RIGHT
            else -> UNDEFINED
        }
    }
}


/**
 * 计算菜单的尺寸
 */
fun calculationMenuSize(menu: JPopupMenu): Dimension {
    var menuHeight = 0
    var menuWidth = 0
    for (component in menu.components) {
        if (component is JMenuItem && component.isVisible) {
            val size = component.getPreferredSize()
            menuHeight += size.height
            menuWidth += 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 true
            if (other !is Order) return false

            if (key != other.key) return false

            return 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()
    }

    @Composable
    private fun rememberOrder(): Int {
        if (!isSort) return -1
        val 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) return
        val orderKey = getOrderKey()
        val list = COM[orderKey] ?: return
        if (list.isEmpty()) return
        list.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 点击菜单项时的回调函数
     */
    @Composable
    fun 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 = enabled
            style.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 点击标签时的回调函数
     */
    @Composable
    fun 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表示默认排序
     */
    @Composable
    fun 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表示默认排序
     */
    @Composable
    fun 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 复选框状态变化时的回调函数
     */
    @Composable
    fun 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 = enabled
            style.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 单选按钮状态变化时的回调函数
     *
     */
    @Composable
    fun 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 = enabled
            style.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 菜单内容的组合构建器
     *
     */
    @Composable
    fun 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 = visible
            isEnabled = enabled

            if (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")
    @Composable
    fun 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")
    @Composable
    fun 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) return
        if (menu.components.count { !it.isVisible } <= 9) {
            for (i in 0..10) {
                menu.add(JMenuItem("Null").apply {
                    isVisible = false
                })
            }
        }

        if (menuItem != null) {
            var count = 0
            var composeCount = 0
            for (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.background
        it.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 = this
        if (font != null) component.font = font
        if (foreground != null) component.foreground = foreground.toAwtColor()
        if (background != null) component.background = background.toAwtColor()
        if (border != null) component.border = border
        if (size != null) component.size = this.size
        if (location != null) component.location = this.location
        if (margin != null) component.margin = margin
        if (bounds != null) component.bounds = bounds
        component.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 = this
        if (font != null) component.font = font
        if (foreground != null) component.foreground = foreground.toAwtColor()
        if (background != null) component.background = background.toAwtColor()
        if (border != null) component.border = border
        if (size != null) component.size = this.size
        if (location != null) component.location = this.location
        if (bounds != null) component.bounds = bounds
        component.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 GlobalDensity
    get() = GraphicsEnvironment.getLocalGraphicsEnvironment()
        .defaultScreenDevice
        .defaultConfiguration
        .density
private val GraphicsConfiguration.density: Density
    get() = Density(
        defaultTransform.scaleX.toFloat(),
        fontScale = 1f
    )

internal val GlobalLayoutDirection get() = Locale.getDefault().layoutDirection
internal val Locale.layoutDirection: LayoutDirection
    get() = ComponentOrientation.getOrientation(this).layoutDirection
internal val ComponentOrientation.layoutDirection: LayoutDirection
    get() = when {
        isLeftToRight -> LayoutDirection.Ltr
        isHorizontal -> LayoutDirection.Rtl
        else -> 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-54572a5d5f87
    DesktopPlatform.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 -> Linux
                name?.startsWith("Win") == true -> Windows
                name == "Mac OS X" -> MacOS
                else -> Unknown
            }
        }
    }
}

private fun androidx.compose.ui.graphics.Color.toAwtColor(): Color = Color(this.red, this.green, this.blue, this.alpha)

使用示例

kotlin 复制代码
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.JButton

fun 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 = true
            WindowState.isMinimized = false
        },
        onClick = {
            if (!isWindowShow.value) isWindowShow.value = true
            WindowState.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))
    }
}
相关推荐
你不是我我30 分钟前
【Java开发日记】说一说 SpringBoot 中 CommandLineRunner
java·开发语言·spring boot
yuan1999731 分钟前
Spring Boot 启动流程及配置类解析原理
java·spring boot·后端
2301_8076064333 分钟前
Java——抽象、接口(黑马个人听课笔记)
java·笔记
楚歌again1 小时前
【如何在IntelliJ IDEA中新建Spring Boot项目(基于JDK 21 + Maven)】
java·spring boot·intellij-idea
酷爱码1 小时前
IDEA 中 Maven Dependencies 出现红色波浪线的原因及解决方法
java·maven·intellij-idea
Magnum Lehar1 小时前
vulkan游戏引擎test_manager实现
java·算法·游戏引擎
sss191s1 小时前
校招 java 面试基础题目及解析
java·开发语言·面试
异常君1 小时前
MySQL 中 count(*)、count(1)、count(字段)性能对比:一次彻底搞清楚
java·mysql·面试
wkj0012 小时前
QuaggaJS 配置参数详解
java·linux·服务器·javascript·quaggajs
异常君2 小时前
MyBatis 中 SqlSessionFactory 和 SqlSession 的线程安全性深度分析
java·面试·mybatis