只在 NSStatusBarButton 右键单击时显示 NSMenu?

12

我有以下代码:(可以复制粘贴到新的macOS项目中)

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    var statusBarItem: NSStatusItem!

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        let statusBar = NSStatusBar.system
        statusBarItem = statusBar.statusItem(
            withLength: NSStatusItem.squareLength)
        statusBarItem.button?.title = ""

        // Setting action
        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp])

        let statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")

        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")

        // Setting menu
        statusBarItem.menu = statusBarMenu
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!

        if event.type ==  NSEvent.EventType.rightMouseUp {
            print("Right click!")
        } else {
            print("Left click!")
        }
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }


    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

实际行为:在左键和右键单击时都会打开菜单,但不会触发statusBarButtonClicked事件。
删除此行后:

statusBarItem.menu = statusBarMenu

在左键单击时,statusBarButtonClicked被触发,菜单不会弹出(正如预期)

期望的行为:右键单击打开菜单,左键单击不打开菜单,触发操作。
我该如何实现?

编辑

在@red_menace的评论帮助下,我成功实现了期望的行为:

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    var statusBarItem: NSStatusItem!
    var menu: NSMenu!

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        let statusBar = NSStatusBar.system
        statusBarItem = statusBar.statusItem(
            withLength: NSStatusItem.squareLength)
        statusBarItem.button?.title = ""

        // Setting action
        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])

        let statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")

        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")

        // Setting menu
        menu = statusBarMenu
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!

        if event.type ==  NSEvent.EventType.rightMouseUp {
            statusBarItem.popUpMenu(menu)
        } else {
            print("Left click!")
        }
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }


    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

但是Xcode说在10.14中openMenu函数已经被弃用,并告诉我要使用menu属性。有没有一种方法可以使用新API实现所需的行为?


1
sendActionOn:中添加NSRightMouseUpMask,不要将菜单设置为状态栏项。然后,在操作方法中检查事件并使用popUpStatusItemMenu:来显示菜单,如果事件是NSEventTypeRightMouseUp。请注意,在Catalina中,这些方法中的几个已被弃用。 - red_menace
@red_menace 感谢您的回答 :). 您是指 popUpMenu: 吗?我找不到 popUpStatusItemMenu。如果是这样,popUpMenu 可以使用,但在 10.14 中已被弃用。警告显示“改用 menu 属性”。 - bapafes482
1
抱歉,popUpStatusItemMenu 是 Obj-C 的写法,Swift 中应该使用 popUpMenu。在 Catalina 中,popUpMenusendAction 已经被弃用,是 NSStatusMenu 的方法。虽然 NSButton 上有一个 sendAction 方法,但是相当于 popUpMenu 的东西应该是设置状态栏菜单并在操作方法中显示它(例如通过 NSControl 的 performClick),然后在完成后删除菜单,以免被用于状态栏项目的正常行为。我不太了解 Swift,无法提供一个好的示例。 - red_menace
5个回答

20
通常展示菜单的方法是将菜单分配给状态项,当单击状态项按钮时,它将被显示出来。由于popUpMenu已过时,需要另一种方法在不同条件下展示菜单。如果您希望右键单击使用实际的状态项菜单而不仅仅是在状态项位置显示上下文菜单,则可以将状态项菜单属性保持为nil,直到您想要显示它。
我已经修改了你的代码,将statusBarItemstatusBarMenu引用分开,并只在点击操作方法中将菜单添加到状态项中。在操作方法中,一旦菜单被添加,将对状态按钮执行正常点击以放下菜单。由于状态项目将始终在单击按钮时显示其菜单,因此添加了一个NSMenuDelegate方法来在关闭菜单时将菜单属性设置为nil,从而恢复原始操作。
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
    // keep status item and menu separate
    var statusBarItem: NSStatusItem!
    var statusBarMenu: NSMenu!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let statusBar = NSStatusBar.system
        statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
        statusBarItem.button?.title = ""

        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])

        statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.delegate = self
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")
        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!
        if event.type ==  NSEvent.EventType.rightMouseUp {
            print("Right click!")
            statusBarItem.menu = statusBarMenu // add menu to button...
            statusBarItem.button?.performClick(nil) // ...and click
        } else {
            print("Left click!")
        }
    }

    @objc func menuDidClose(_ menu: NSMenu) {
        statusBarItem.menu = nil // remove menu so button works as before
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }

    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

你的方法对我不起作用。只有当我删除 statusBarItem.button?.performClick(nil) 这行代码时,菜单才会显示出来。而且,当我右键点击按钮两次时,菜单才会显示出来。你知道另一种方法可以在不使用 performClick(nil) 的情况下显示菜单吗? - D0miH
“不起作用”到底是什么意思?你是否收到了错误提示?日志中是否有任何信息? - red_menace
好的,我知道为什么它没有起作用。我使用了外部显示器,显然在调用performClick()时,点击是在外部显示器上执行而不是我的笔记本内置显示器上执行。现在我只需要找到一种方法在正确的显示器上执行点击... - D0miH
点击应该在状态按钮上执行,无论它在哪里。 - red_menace
我认为这种情况的问题在于使用外部显示器时会有两个状态按钮(每个显示器一个)。如果我找到了解决方法,我会在这里发布一个解决方案。 - D0miH
好的,对于任何遇到相同问题的人来说,似乎这是macOS Catalina中的一个错误:https://forums.developer.apple.com/thread/126072 希望他们能尽快修复它。 - D0miH

5

这里有一种可能的方法。关于菜单位置的计算可能还有更准确的方法,包括考虑到 userInterfaceLayoutDirection 的可能差异,但是思路是一样的 - 将可能发生的事件手动控制并根据每个事件做出自己的决定。

代码中有重要的地方进行了注释。(在 Xcode 11.2,macOS 10.15 上测试通过)

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    var statusBarItem: NSStatusItem!

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        let statusBar = NSStatusBar.system
        statusBarItem = statusBar.statusItem(
            withLength: NSStatusItem.squareLength)
        statusBarItem.button?.title = ""

        // Setting action
        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp]) // << send action in both cases

        let statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")

        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")

        // Setting menu
        statusBarItem.button?.menu = statusBarMenu // << store menu in button, not item
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!

        if event.type ==  NSEvent.EventType.rightMouseUp {
            print("Right click!")
            if let button = statusBarItem.button { // << pop up menu programmatically
                button.menu?.popUp(positioning: nil, at: CGPoint(x: -1, y: button.bounds.maxY + 5), in: button)
            }
        } else {
            print("Left click!")
        }
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }


    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

谢谢。 :) 只有一个问题,为什么菜单的左上角和右上角有圆角边框?如果我尝试在y轴上重新定位它,它会重叠在菜单栏上。https://i.stack.imgur.com/KtTSY.png - bapafes482

3
我正在使用这段代码在macOS Catalina上,版本为10.15.2(Xcode 11.3)。
左键单击它将触发动作。
右键单击它将显示菜单。
//HEADER FILE
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN

@protocol MOSMainStatusBarDelegate

- (void) menuBarControllerStatusChanged: (BOOL) active;

@end


@interface MOSMainStatusBar : NSObject

@property (strong) NSMenu *menu;
@property (strong, nonatomic) NSImage *image;
@property (unsafe_unretained, nonatomic) id<MOSMainStatusBarDelegate> delegate;

- (instancetype) initWithImage: (NSImage *) image menu: (NSMenu *) menu;
- (NSStatusBarButton *) statusItemView;
- (void) showStatusItem;
- (void) hideStatusItem;


@end

//IMPLEMANTION FILE. 

#import "MOSMainStatusBar.h"

@interface MOSMainStatusBar ()

@property (strong, nonatomic) NSStatusItem *statusItem;

@end

@implementation MOSMainStatusBar

- (instancetype) initWithImage: (NSImage *) image menu: (NSMenu *) menu {
    self = [super init];
    if (self) {
        self.image = image;
        self.menu = menu;
    }
    return self;
}

- (void) setImage: (NSImage *) image {
    _image = image;
    self.statusItem.button.image = image;

}

- (NSStatusBarButton *) statusItemView {
    return self.statusItem.button;
}

- (void) showStatusItem {
    if (!self.statusItem) {
        self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];

        [self initStatusItem10];

    }
}

- (void) hideStatusItem {
    if (self.statusItem) {
        [[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
        self.statusItem = nil;
    }
}



- (void) initStatusItem10 {

    self.statusItem.button.image = self.image;


    self.statusItem.button.imageScaling =  NSImageScaleAxesIndependently;

    self.statusItem.button.appearsDisabled = NO;
    self.statusItem.button.target = self;
    self.statusItem.button.action = @selector(leftClick10:);

    __unsafe_unretained MOSMainStatusBar *weakSelf = self;

    [NSEvent addLocalMonitorForEventsMatchingMask:
     (NSEventMaskRightMouseDown | NSEventModifierFlagOption | NSEventMaskLeftMouseDown) handler:^(NSEvent *incomingEvent) {

         if (incomingEvent.type == NSEventTypeLeftMouseDown) {
             weakSelf.statusItem.menu = nil;
         }

         if (incomingEvent.type == NSEventTypeRightMouseDown || [incomingEvent modifierFlags] & NSEventModifierFlagOption) {

             weakSelf.statusItem.menu = weakSelf.menu;
         }

         return incomingEvent;
     }];
}

- (void)leftClick10:(id)sender {

    [self.delegate menuBarControllerStatusChanged:YES];
}

我的 objc 知识甚至比 swift 还要差,但我可以看到你是在右键单击时设置菜单,并在左键单击时将其设置为 nil。老实说,这看起来有点像肮脏的解决方法,因为 NSMenu 提供了 popUp 函数(Asperi 的答案)。但也许在 macOS 开发中,你的方法被认为是可以接受的。我错了吗? - bapafes482
2
这是我使用的代码(我从网上获取了基础知识,并在自己的类中进行了设计)。如果你需要右键和左键,这是可行的代码。我在我的应用程序中使用它已经10个月了,从未注意到任何问题。我没有看到这段代码中有任何“脏”东西。我不明白另一个答案中的菜单位置。在我的代码中,我不会“触及”位置=减少未来的问题。 - user1105951

2

我在Catalina上使用这段代码。与其他答案建议的performClick不同,我必须手动定位弹出窗口。这对我在外部显示器上都有效。

func applicationDidFinishLaunching(_ aNotification: Notification) {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusItem.button?.action = #selector(onClick)
        statusItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
        
        menu = NSMenu()
        menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
        menu.delegate = self
        
    }
    
    @objc func onClick(sender: NSStatusItem) {
        
        let event = NSApp.currentEvent!
       
        if event.type == NSEvent.EventType.rightMouseUp {
            // right click, show quit menu
            statusItem.menu = menu;
            menu.popUp(positioning: nil, 
                at: NSPoint(x: 0, y: statusItem.statusBar!.thickness),
                in: statusItem.button)
        } else {
           // main click 
        }
    }
    @objc func menuDidClose(_ menu: NSMenu) {
        // remove menu when closed so we can override left click behavior
        statusItem.menu = nil
    }

0

从这里已经给出的伟大答案中学到了一些东西,我想到了一个替代方案。

这种方法的优点在于您只需设置一次NSMenu,不必再与设置或删除它作斗争。

设置NSStatusBarButton

let status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
status.button!.image = NSImage(named: "status")
status.button!.target = self
status.button!.action = #selector(triggerStatus)
status.button!.menu = /* your NSMenu */
status.button!.sendAction(on: [.leftMouseUp, .rightMouseUp])

接收动作

@objc private func triggerStatus(_ button: NSStatusBarButton) {
    guard let event = NSApp.currentEvent else { return }
        
    switch event.type {
    case .rightMouseUp:
        NSMenu.popUpContextMenu(button.menu!, with: event, for: button)
    case .leftMouseUp:
            /* show your Popup or any other action */
    default:
        break
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接