You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI MenuBarExtra实现托盘图标点击切换主窗口方案咨询

SwiftUI MenuBarExtra 实现托盘图标点击切换窗口+右键菜单方案

核心实现方案

可以通过 SwiftUI MenuBarExtra 结合 AppKit 底层 API 实现需求,无需完全切换到原生 NSMenu 方案,既保留 SwiftUI 声明式开发的便捷性,又能满足托盘交互的定制需求。

1. 初始隐藏主窗口 + 点击托盘切换显示/隐藏(居中在图标下方)

通过管理窗口显示状态,结合 NSStatusItem 处理左键点击事件,同时计算托盘图标位置来定位窗口:

import SwiftUI

@main
struct TrayApp: App {
    @State private var isWindowVisible = false
    private var statusItem: NSStatusItem?
    
    var body: some Scene {
        // 主窗口:初始隐藏,可自定义样式
        WindowGroup {
            ContentView()
        }
        .windowStyle(.hiddenTitleBar)
        .onAppear {
            NSApplication.shared.windows.first?.isHidden = true
        }
        
        // 托盘项:设置为window样式,避免默认弹出菜单
        MenuBarExtra("Tray", systemImage: "square.fill") {
            // 此处留空,通过NSStatusItem自定义左键行为
        }
        .menuBarExtraStyle(.window)
        .onAppear {
            // 匹配当前应用的托盘项
            statusItem = NSApplication.shared.statusItems.first { item in
                item.button?.image == NSImage(systemSymbolName: "square.fill", accessibilityDescription: nil)
            }
            // 绑定左键点击事件
            statusItem?.button?.action = #selector(toggleWindow)
            statusItem?.button?.target = self
            // 初始化右键菜单
            setupRightClickMenu()
        }
    }
    
    @objc private func toggleWindow() {
        guard let mainWindow = NSApplication.shared.windows.first else { return }
        isWindowVisible.toggle()
        
        if isWindowVisible {
            mainWindow.isHidden = false
            mainWindow.makeKeyAndOrderFront(nil)
            // 计算窗口在托盘下方的居中位置
            if let buttonFrame = statusItem?.button?.frame, let screenFrame = NSScreen.main?.frame {
                let windowX = buttonFrame.origin.x + buttonFrame.width/2 - mainWindow.frame.width/2
                let windowY = buttonFrame.origin.y - mainWindow.frame.height
                // 避免窗口超出屏幕边界
                let safeX = max(min(windowX, screenFrame.width - mainWindow.frame.width), 0)
                let safeY = max(windowY, 0)
                mainWindow.setFrameOrigin(NSPoint(x: safeX, y: safeY))
            }
        } else {
            mainWindow.isHidden = true
        }
    }
    
    private func setupRightClickMenu() {
        let menu = NSMenu()
        menu.addItem(withTitle: "关于", action: #selector(showAboutPanel), keyEquivalent: "")
        menu.addItem(NSMenuItem.separator())
        menu.addItem(withTitle: "退出", action: #selector(quitApp), keyEquivalent: "")
        statusItem?.menu = menu
    }
    
    @objc private func showAboutPanel() {
        NSApplication.shared.orderFrontStandardAboutPanel(nil)
    }
    
    @objc private func quitApp() {
        NSApplication.shared.terminate(nil)
    }
}

struct ContentView: View {
    var body: some View {
        Text("主窗口内容")
            .frame(width: 220, height: 160)
            .padding()
    }
}

关键细节说明

  • 使用 .menuBarExtraStyle(.window):让托盘项不默认弹出菜单,允许通过 NSStatusItem 自定义左键交互
  • 窗口位置计算:通过托盘按钮的屏幕坐标,计算窗口在其下方的居中位置,并添加边界判断避免超出屏幕
  • 状态匹配:通过图标匹配当前应用的托盘项,若存在多个托盘图标,可通过设置唯一 accessibilityLabel 来精准匹配

2. 右键菜单实现

通过 NSMenu 创建右键菜单项,绑定对应的操作(关于、退出),并赋值给 NSStatusItem 的 menu 属性,即可实现右键点击托盘图标弹出菜单的效果。

是否需要切换到原生macOS托盘菜单?

无需完全切换。当前混合方案(SwiftUI MenuBarExtra + AppKit API)既能利用 SwiftUI 的开发效率,又能通过 AppKit 实现托盘交互的定制需求,比纯原生 NSStatusItem 方案更简洁高效。

内容的提问来源于stack exchange,提问作者Jordan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 06:13:21