创建按钮menubar_SwiftUI与macOS开发:menuBar/statusBar详解

本文详细介绍了如何使用SwiftUI在macOS应用中开发menuBar和statusBar,包括在statusBarItem上弹出Popover,自定义Popover背景色,以及在statusBar上添加控制主窗口和插件开关的按钮。通过示例代码,帮助开发者掌握SwiftUI在macOS上的高级用法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

macOS的menuBar功能非常强大,我们在开发macOS应用的时候,经常需要利用menuBar实现功能。然而网上关于menuBar开发的文档却甚少,更别提用上SwiftUI开发的。这篇文章从以下几部分讲解用SwiftUI开发menuBar/statusBar的方法,希望对大家有帮助:

一、在statusBarItem弹出Popover

二、更改Popover背景色

三、在statusBar上控制主窗口的开关

四、在statusBar上控制statusBar的显隐/开关

一、在statusBarItem弹出Popover

statusBar弹出menu是比较简单的,但是如果我们要做一些定制,就需要用到Popover了

创建Popover

在AppDelegate文件中,声明一个NSPopover变量(下面的statusBarItem之后会用到)

var popover: NSPopover! var statusBarItem: NSStatusItem!

随后,在applicationDidFinishLaunching中, ContentView 初始化之后,加上

// Create the popover  
let popover = NSPopover()  
popover.contentSize = NSSize(width: 400, height: 400)  
popover.behavior = .transient //代表用户点击其他区域时popover自动消失 
popover.contentViewController = NSHostingController(rootView: contentView) 
self.popover = popover

此时我们的AppDelegate文件应该长这样:

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var popover: NSPopover!
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Create the popover
        let popover = NSPopover()
        popover.contentSize = NSSize(width: 400, height: 500)
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover
    }
    
}

这样我们即创建了一个Popover视图以及它的控制器,这个Popover的视图内容是我们的ContentView.swift里的内容。这样我们就可以在ContentView里用SwiftUI自由地定制我们的Popover。接下来我们要做的是,创建一个menuBarItem,当用户点击menuBarItem的按钮时,弹出我们已经写好的Popover。

self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
     button.image = NSImage(named: "Icon")
     button.action = #selector(togglePopover(_:))
}

还记得我们一开始声明了

var statusBarItem: NSStatusItem!

现在我们需要创建它,同时给它在menuBar设置个图标,绑定点击事件为togglePopover

self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
     statusBarItem.button?.title = "⏳"
     button.action = #selector(togglePopover(_:))
}

绑定togglePopover是为了在用户点击时弹出Popover

// Create the status item
@objc func togglePopover(_ sender: AnyObject?) {
     if let button = self.statusBarItem.button {
          if self.popover.isShown {
               self.popover.performClose(sender)
          } else {
               self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
          }
     }
}

最终,我们的AppDelegate文件长这样

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var popover: NSPopover!
    var statusBarItem: NSStatusItem!
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Create the popover
        let popover = NSPopover()
        popover.contentSize = NSSize(width: 400, height: 500)
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover
        
        // Create the status item
        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
        
        if let button = self.statusBarItem.button {
            statusBarItem.button?.title = "⏳"
            button.action = #selector(togglePopover(_:))
        }
    }
    
    @objc func togglePopover(_ sender: AnyObject?) {
        if let button = self.statusBarItem.button {
            if self.popover.isShown {
                self.popover.performClose(sender)
            } else {
                self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
            }
        }
    }
    
}

二、更改Popover背景色

我们创建出来的Popover默认的背景色是透明的,很容易与我们的UI不搭配。如果需要定制背景颜色,目前我没有发现直接用SwiftUI实现的方法。我们需要去拓展NSPopover类,在新建实例的时候指定背景色。

首先,拓展NSPopover类:

import Cocoa
import SwiftUI
extension NSPopover {
    
    private struct Keys {
        static var backgroundViewKey = "backgroundKey"
    }
    
    private var backgroundView: NSView {
        let bgView = objc_getAssociatedObject(self, &Keys.backgroundViewKey) as? NSView
        if let view = bgView {
            return view
        }
        
        let view = NSView()
        objc_setAssociatedObject(self, &Keys.backgroundViewKey, view, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        NotificationCenter.default.addObserver(self, selector: #selector(popoverWillOpen(_:)), name: NSPopover.willShowNotification, object: nil)
        return view
    }
    
    @objc private func popoverWillOpen(_ notification: Notification) {
        if backgroundView.superview == nil {
            if let contentView = contentViewController?.view, let frameView = contentView.superview {
                frameView.wantsLayer = true
                backgroundView.frame = NSInsetRect(frameView.frame, 1, 1)
                backgroundView.autoresizingMask = [.width, .height]
                frameView.addSubview(backgroundView, positioned: .below, relativeTo: contentView)
            }
        }
    }
    
    var backgroundColor: NSColor? {
        get {
            if let bgColor = backgroundView.layer?.backgroundColor {
                return NSColor(cgColor: bgColor)
            }
            return nil
        }
        set {
            backgroundView.wantsLayer = true
            backgroundView.layer?.backgroundColor = newValue?.cgColor
        }
    }
    
}

在AppDelegate初始化时,增加background参数

90e95494ed30c81cbbcd4f7167fe7a46.png
let popover = NSPopover()
        popover.backgroundColor = NSColor.white   //设置popover颜色,对应extension里对popover的改写
        popover.contentSize = NSSize(width: 350, height: 400)

搞定!

eec9b1926a4876d8455a69de5d81e8e3.png

90e95494ed30c81cbbcd4f7167fe7a46.png

三、在statusBar增加控制主窗口开关和插件开关

现在statusBarItem的视图文件中加入按钮

HStack{
                Text("Time Capsule").font(Font.system(.headline)).bold()
                Spacer()
                MenuButton(label: Image("moreBlack").resizable().frame(width:20,height:20)) {
                    Button(action: {
                      showWindow()
                    }) {
                        Text("打开主窗口")
                    }
                    Button(action: {
                        hideStatusBar()
                    }) {
                        Text("退出")
                    }
                }.menuButtonStyle(BorderlessButtonMenuButtonStyle())
                
            }
            .padding([.top, .trailing], 18).padding(.leading,30)

接下来我们要补充两个按钮点击事件showWindow()和hideStatusBar()的方法

func showWindow() {
    NSApp.unhide(nil)

func hideStatusBar() {
    let appDelegate = NSApplication.shared.delegate as! AppDelegate
    appDelegate.statusBarItem.isVisible = false
}

NSApp.unhide方法可以直接显示我们的主程序,hide则可以隐藏主程序

statusBarItem.isVisible设为false则可以直接让我们的插件消失

把一下PyQt5的代码变成PyQt6的代码 代码: from PyQt5.QtCore import QUrl, Qt, QTranslator, QLocale, QLibraryInfo from PyQt5.QtGui import QFont, QIcon, QKeySequence from PyQt5.QtWidgets import (QApplication, QMainWindow, QToolBar, QLineEdit, QAction, QStatusBar, QMessageBox, QTabWidget) from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage, QWebEngineSettings from datetime import datetime import sys,os os.environ["QT_MULTIMEDIA_PREFERRED_PLUGINS"] = "windowsmediafoundation" os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--enable-gpu-rasterization --enable-zero-copy" os.environ["QT_MEDIA_BACKEND"] = "ffmpeg" class CustomWebView(QWebEngineView): def __init__(self, parent=None,window=None): super().__init__(parent) # 捕获链接悬停事件获取URL self.window = window self.page().linkHovered.connect(self.save_hovered_url) self.hovered_url = QUrl() self.page().profile().setHttpUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") settings = self.settings() settings.setAttribute(QWebEngineSettings.PlaybackRequiresUserGesture, False) # 允许自动播放 settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) # 启用插件 settings.setAttribute(QWebEngineSettings.WebGLEnabled, True) # 支持WebGL settings.setAttribute(QWebEngineSettings.LocalStorageEnabled, True) # 启用本地存储 settings.setAttribute(QWebEngineSettings.JavascriptCanOpenWindows, True) def save_hovered_url(self, url): """保存鼠标悬停的链接地址""" self.hovered_url = QUrl(url) def createWindow(self, type): """重写窗口创建方法""" url = QUrl(self.url().toString()) if self.hovered_url.isValid(): try: self.window.add_new_tab(self.hovered_url) self.load(url) except: self.load(self.hovered_url) return self # 返回当前视图实例 def save_screenshot(self): current_time = datetime.now() photo_name = current_time.strftime("%Y%m%d_%H%M%S_%f") + '.png' save_folder = r'save\photo' os.makedirs(save_folder, exist_ok=True) save_path = os.path.join(save_folder, photo_name) photo = self.grab() photo.save(rf'save\photo\{photo_name}') class BrowserWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PyQt5 浏览器") self.setGeometry(100, 100, 1200, 800) # 创建标签页 self.tabs = QTabWidget() self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect(self.close_tab) self.setCentralWidget(self.tabs) # 创建初始标签页 self.add_new_tab(QUrl('https://www.baidu.com'), "主页") # 创建工具栏 self.create_toolbar() # 创建菜单栏 self.create_menus() # 创建状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) # 显示窗口 self.show() def add_new_tab(self, url=None, label="新标签页"): """添加新标签页""" if url is None: url = QUrl('https://www.baidu.com') browser = CustomWebView(window=self) settings = browser.settings() settings.setFontFamily(QWebEngineSettings.StandardFont, 'Microsoft YaHei UI') settings.setFontFamily(QWebEngineSettings.FixedFont, "Courier New") browser.setUrl(url) browser.urlChanged.connect(self.update_urlbar) browser.loadProgress.connect(self.update_progress) browser.loadFinished.connect(self.update_title) index = self.tabs.addTab(browser, label) self.tabs.setCurrentIndex(index) return browser def close_tab(self, index): """关闭标签页""" if self.tabs.count() < 2: return self.tabs.removeTab(index) def create_toolbar(self): """创建工具栏""" toolbar = QToolBar("导航工具栏") toolbar.setMovable(False) self.addToolBar(toolbar) # 后退按钮 back_btn = QAction(QIcon("back.png"), "后退", self) back_btn.setStatusTip("后退到上一页") back_btn.triggered.connect(lambda: self.current_browser().back()) toolbar.addAction(back_btn) # 前进按钮 forward_btn = QAction(QIcon("forward.png"), "前进", self) forward_btn.setStatusTip("前进到下一页") forward_btn.triggered.connect(lambda: self.current_browser().forward()) toolbar.addAction(forward_btn) # 刷新按钮 reload_btn = QAction(QIcon("reload.png"), "刷新", self) reload_btn.setStatusTip("刷新当前页面") reload_btn.triggered.connect(lambda: self.current_browser().reload()) toolbar.addAction(reload_btn) # 主页按钮 home_btn = QAction(QIcon("home.png"), "主页", self) home_btn.setStatusTip("返回主页") home_btn.triggered.connect(self.navigate_home) toolbar.addAction(home_btn) toolbar.addSeparator() # 地址栏 self.urlbar = QLineEdit() self.urlbar.returnPressed.connect(self.navigate_to_url) toolbar.addWidget(self.urlbar) # 停止加载按钮 stop_btn = QAction(QIcon("stop.png"), "停止", self) stop_btn.setStatusTip("停止加载当前页面") stop_btn.triggered.connect(lambda: self.current_browser().stop()) toolbar.addAction(stop_btn) def create_menus(self): """创建菜单栏""" # 文件菜单 file_menu = self.menuBar().addMenu("文件(&F)") new_tab_action = QAction(QIcon("new-tab.png"), "新建标签页", self) new_tab_action.setShortcut(QKeySequence.AddTab) new_tab_action.triggered.connect(lambda: self.add_new_tab()) file_menu.addAction(new_tab_action) new_window_action = QAction(QIcon("new-window.png"), "新建窗口", self) new_window_action.setShortcut("Ctrl+N") new_window_action.triggered.connect(self.new_window) file_menu.addAction(new_window_action) file_menu.addSeparator() save_page_action = QAction(QIcon("save.png"), "保存页面", self) save_page_action.setShortcut("Ctrl+S") save_page_action.triggered.connect(self.save_page) file_menu.addAction(save_page_action) file_menu.addSeparator() exit_action = QAction(QIcon("exit.png"), "退出", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # 编辑菜单 edit_menu = self.menuBar().addMenu("编辑(&E)") copy_action = QAction(QIcon("copy.png"), "复制", self) copy_action.setShortcut("Ctrl+C") copy_action.triggered.connect(self.copy) edit_menu.addAction(copy_action) paste_action = QAction(QIcon("paste.png"), "粘贴", self) paste_action.setShortcut("Ctrl+V") paste_action.triggered.connect(self.paste) edit_menu.addAction(paste_action) cut_action = QAction(QIcon("cut.png"), "剪切", self) cut_action.setShortcut("Ctrl+X") cut_action.triggered.connect(self.cut) edit_menu.addAction(cut_action) # 查看菜单 view_menu = self.menuBar().addMenu("查看(&V)") zoom_in_action = QAction(QIcon("zoom-in.png"), "放大", self) zoom_in_action.setShortcut("Ctrl++") zoom_in_action.triggered.connect(self.zoom_in) view_menu.addAction(zoom_in_action) zoom_out_action = QAction(QIcon("zoom-out.png"), "缩小", self) zoom_out_action.setShortcut("Ctrl+-") zoom_out_action.triggered.connect(self.zoom_out) view_menu.addAction(zoom_out_action) reset_zoom_action = QAction(QIcon("zoom-reset.png"), "重置缩放", self) reset_zoom_action.setShortcut("Ctrl+0") reset_zoom_action.triggered.connect(self.reset_zoom) view_menu.addAction(reset_zoom_action) # 历史菜单 history_menu = self.menuBar().addMenu("历史(&H)") back_action = QAction(QIcon("back.png"), "后退", self) back_action.setShortcut("Alt+Left") back_action.triggered.connect(lambda: self.current_browser().back()) history_menu.addAction(back_action) forward_action = QAction(QIcon("forward.png"), "前进", self) forward_action.setShortcut("Alt+Right") forward_action.triggered.connect(lambda: self.current_browser().forward()) history_menu.addAction(forward_action) # 帮助菜单 help_menu = self.menuBar().addMenu("帮助(&H)") about_action = QAction(QIcon("about.png"), "关于", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def current_browser(self): """获取当前标签页的浏览器实例""" return self.tabs.currentWidget() def navigate_home(self): """导航到主页""" self.current_browser().setUrl(QUrl("https://www.baidu.com")) def navigate_to_url(self): """导航到地址栏中的URL""" url = self.urlbar.text() if not url.startswith(('http://', 'https://')): url = 'http://' + url self.current_browser().setUrl(QUrl(url)) def update_urlbar(self, q): """更新地址栏显示""" self.urlbar.setText(q.toString()) self.urlbar.setCursorPosition(0) def update_progress(self, progress): """更新加载进度""" if progress < 100: self.status_bar.showMessage(f"加载中: {progress}%") else: self.status_bar.clearMessage() def update_title(self): """更新标签页标题""" title = self.current_browser().page().title() self.setWindowTitle(f"{title} - PyQt5 浏览器") self.tabs.setTabText(self.tabs.currentIndex(), title[:15] + '...' if len(title) > 15 else title) def new_window(self): """创建新窗口""" new_window = BrowserWindow() new_window.show() def save_page(self): """保存页面""" self.current_browser().save_screenshot() self.status_bar.showMessage("保存成功", 3000) def copy(self): """复制操作""" self.current_browser().page().triggerAction(QWebEnginePage.Copy) def paste(self): """粘贴操作""" self.current_browser().page().triggerAction(QWebEnginePage.Paste) def cut(self): """剪切操作""" self.current_browser().page().triggerAction(QWebEnginePage.Cut) def zoom_in(self): """放大页面""" self.current_browser().setZoomFactor(self.current_browser().zoomFactor() + 0.1) def zoom_out(self): """缩小页面""" self.current_browser().setZoomFactor(self.current_browser().zoomFactor() - 0.1) def reset_zoom(self): """重置缩放""" self.current_browser().setZoomFactor(1.0) def show_about(self): """显示关于对话框""" QMessageBox.about(self, "关于 PyQt5 浏览器", "这是一个使用 PyQt5 创建的浏览器\n\n" "版本: 1.0\n" "支持基本浏览功能、多标签页和菜单栏操作") if __name__ == "__main__": app = QApplication(sys.argv) app.setApplicationName("PyQt5 浏览器") app.setWindowIcon(QIcon("browser.png")) app.setFont(QFont('Microsoft YaHei UI', 10, QFont.Normal)) translator = QTranslator() locale = QLocale.system().name() if translator.load("qt_"+ locale, QLibraryInfo.location(QLibraryInfo.TranslationsPath)): app.installTranslator(translator) # 设置默认样式 app.setStyle("Fusion") window = BrowserWindow() sys.exit(app.exec_())
07-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值