【问题标题】:Show NSMenu only on NSStatusBarButton right click?仅在 NSStatusBarButton 右键单击​​时显示 NSMenu?
【发布时间】:2020-01-07 20:58:04
【问题描述】:

我有以下代码:(可以复制粘贴到新的 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 说 openMenu func 在 10.14 中已弃用,并告诉我 Use the menu property instead。有没有办法通过新的 API 实现所需的行为?

【问题讨论】:

  • NSRightMouseUpMask添加到sendActionOn:,不要将菜单设置为statusBarItem。然后只需检查 action 方法中的事件,如果事件为NSEventTypeRightMouseUp,则使用popUpStatusItemMenu: 显示菜单。请注意,其中一些方法在 Catalina 中已被弃用。
  • @red_menace 感谢您的回答 :)。你是说popUpMenu:?我找不到popUpStatusItemMenu。如果是这样,popUpMenu 有效,但在 10.14 中已弃用。警告显示“改用菜单属性”
  • 对不起,popUpStatusItemMenu 是 Obj-C,Swift 是 popUpMenu。在 Catalina 中,popUpMenusendAction 是不推荐使用的 NSStatusMenu 方法。在 NSButton 上有一个 sendAction 方法,但等效于 popUpMenu 将类似于设置状态项菜单并在操作方法中显示它(例如通过 NSControl 的 performClick),然后在完成后删除菜单,以便它不用于状态项的正常行为。我不太了解 Swift,无法提供一个像样的例子。

标签: swift macos cocoa swiftui appkit


【解决方案1】:

显示菜单的常用方法是为状态项分配一个菜单,单击状态项按钮时将显示该菜单。由于不推荐使用popUpMenu,因此需要另一种方式来在不同条件下显示菜单。如果您希望右键单击以使用实际的状态项菜单,而不是仅在状态项位置显示上下文菜单,则可以将状态项菜单属性保持为 nil,直到您想要显示它。

我已经修改了您的代码以保持 statusBarItemstatusBarMenu 引用分开,仅将菜单添加到单击操作方法中的状态项。在 action 方法中,一旦添加了菜单,就会在状态按钮上执行正常的单击以放下菜单。由于单击按钮时状态项将始终显示其菜单,因此添加了一个 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) 来显示菜单的方法吗?
  • “不起作用”到底是什么意思?你有错误吗?日志里有东西吗?
  • 好的,我知道为什么它不起作用。我使用的是外接显示器,显然在调用performClick() 时,点击是在外接显示器上执行的,而不是我笔记本电脑的内置显示器。现在我只需要找到一种方法来在正确的监视器上执行点击......
  • 点击应该在状态按钮上执行,无论它在哪里。
  • 我认为这种情况下的问题是使用外接显示器时有两个状态按钮(每个显示器一个)。如果我有一个解决方案,我会尝试找到一种方法来完成这项工作并在此处发布解决方案。
【解决方案2】:

这是可能的方法。可能会有更准确的菜单位置计算,包括考虑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 轴上重新定位它,它只会与菜单栏重叠。 i.stack.imgur.com/KtTSY.png
【解决方案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开发中被认为是可以的。我错了吗?
  • 这是我使用的(我从网上获得了基础知识,并在我自己的课堂上设计了它)。如果您需要的是右键单击和左键单击,这是工作代码。我在我的应用程序中使用了 10 多个月,从未发现任何问题。我在这段代码中看不到任何“脏”的东西。另一个答案我不明白的是 menu 的位置。在我的代码中,我不“触摸”位置 = 更少的未来问题。
【解决方案4】:

我在 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
    }

【讨论】:

    【解决方案5】:

    从这里已经给出的很好的答案中学习,我想出了一个替代方案。

    这种方法的优点是您只需设置一次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
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-19
      • 1970-01-01
      • 2017-12-02
      相关资源
      最近更新 更多