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

news2025/6/6 9:29:22

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

使用示例

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2401498.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

风控研发大数据学习路线

在如今信息爆炸时代&#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;任何单位或个人不得以任何形式复制、发行、出租、改编、汇编、传播、展示或利用本博…

深入理解系统:UML类图

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

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

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

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

集合嵌套案例分析 代码&#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…

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时所有的排列情况…

Apache Druid

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

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

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

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

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

复变函数中的对数函数及其MATLAB演示

复变函数中的对数函数及其MATLAB演示 引言 在实变函数中&#xff0c;对数函数 ln ⁡ x \ln x lnx定义在正实数集上&#xff0c;是一个相对简单的概念。然而&#xff0c;当我们进入复变函数领域时&#xff0c;对数函数展现出更加丰富和复杂的性质。本文将介绍复变函数中对数函…

【Linux】Linux程序地址基础

参考博客&#xff1a;https://blog.csdn.net/sjsjnsjnn/article/details/125533127 一、地址空间的阐述 1.1 程序地址空间 下面的图片展示了程序地址空间的组成结构 我们通过代码来验证一下 int g_unval; int g_val 100;int main(int argc, char *argv[]);void test1() {i…

将图形可视化工具的 Python 脚本打包为 Windows 应用程序

前文我们已经写了一个基于python的tkinter库和matplotlib库的图形可视化工具。 基于Python的tkinter库的图形可视化工具&#xff08;15种图形的完整代码&#xff09;:基于Python的tkinter库的图形可视化工具&#xff08;15种图形的完整代码&#xff09;-CSDN博客 在前文基础上&…