具有自定义视图的 NSMenuItem 不接收鼠标事件

Posted

技术标签:

【中文标题】具有自定义视图的 NSMenuItem 不接收鼠标事件【英文标题】:NSMenuItem with custom view doesn't receive mouse events 【发布时间】:2017-11-15 14:29:07 【问题描述】:

我正在开发一个菜单栏应用程序,我正在使用NSMenuItemview 属性设置自定义视图。

视图显示正常,但对于具有打开子菜单的菜单项,我无法接收任何类型的鼠标单击事件。

在此屏幕截图中,我为每个项目添加了一个按钮。最右边的 3 个按钮功能正常,但父菜单中的按钮根本不接收任何点击事件。

我尝试了很多东西,包括:

尝试使用 mouseUpmouseDown 方法捕获鼠标事件 当鼠标进入该视图时为自定义视图键创建NSWindowNSEvents 添加全局和本地监视器

...但无济于事

即使没有添加按钮的方法,我也无法复制标准NSMenuItem 的默认行为,因为如果NSMenuItem 具有自定义视图,则不会调用target-action 回调。 (而且我自己也收不到任何点击事件来调用它)

理论上这应该是可能的,因为我可以使用默认的NSMenuItem(无自定义视图)选择​​具有打开子菜单的菜单。

有人可以帮忙吗?

谢谢

【问题讨论】:

您可以发布添加菜单项时的代码吗?从您的图片看来,您正在向菜单项添加视图(在顶部),而不是使菜单项成为自定义视图。这似乎是一个有趣的问题。 现在在 Big Sur 上有一个非常相似的问题。 NSButton 被添加到我的自定义视图中,但我根本没有收到任何 IBActions。奇怪的是这仍然是一个问题,尤其是因为我还没有找到任何解决方法。 【参考方案1】:

我设置了一个像您一样的测试项目,将NSButtons 作为菜单项的view,并看到了与您看到的相同的行为。这确实很有趣。如果您将NSApplication 子类化并覆盖它的-sendEvent: 方法,添加一个日志以查看哪些事件通过该机制,您会发现当您单击任何菜单项时实际上从未调用-sendEvent:,即使是那些工作。这不是很奇怪吗?所以接下来要尝试的是将NSButton 子类化,为-mouseDown: 添加一个覆盖,并在那里放置一个断点。果然,打开子菜单的项目永远不会打断点,但会为其他项目打断点。当我们这样做时,回溯是:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100002fa0 menutest`MyButton.mouseDown(event=0x0000608000121900, self=0x0000600000140a50) at AppDelegate.swift:33
    frame #1: 0x000000010000303c menutest`@objc MyButton.mouseDown(with:) at AppDelegate.swift:0
    frame #2: 0x00007fffa9f6724f AppKit`-[NSWindow(NSEventRouting) _handleMouseDownEvent:isDelayedEvent:] + 6341
    frame #3: 0x00007fffa9f63a6c AppKit`-[NSWindow(NSEventRouting) _reallySendEvent:isDelayedEvent:] + 1942
    frame #4: 0x00007fffa9f62f0a AppKit`-[NSWindow(NSEventRouting) sendEvent:] + 541
    frame #5: 0x00007fffa9a2328d AppKit`-[NSCarbonWindow sendEvent:] + 118
    frame #6: 0x00007fffa9a20261 AppKit`NSMenuItemCarbonEventHandler + 10597
    frame #7: 0x00007fffab0acd85 HIToolbox`DispatchEventToHandlers(EventTargetRec*, OpaqueEventRef*, HandlerCallRec*) + 1708
    frame #8: 0x00007fffab0abff6 HIToolbox`SendEventToEventTargetInternal(OpaqueEventRef*, OpaqueEventTargetRef*, HandlerCallRec*) + 428
    frame #9: 0x00007fffab0c1d14 HIToolbox`SendEventToEventTarget + 40
    frame #10: 0x00007fffab0ea7df HIToolbox`ToolboxEventDispatcherHandler(OpaqueEventHandlerCallRef*, OpaqueEventRef*, void*) + 2503
    frame #11: 0x00007fffab0ad17a HIToolbox`DispatchEventToHandlers(EventTargetRec*, OpaqueEventRef*, HandlerCallRec*) + 2721
    frame #12: 0x00007fffab0abff6 HIToolbox`SendEventToEventTargetInternal(OpaqueEventRef*, OpaqueEventTargetRef*, HandlerCallRec*) + 428
    frame #13: 0x00007fffab0c1d14 HIToolbox`SendEventToEventTarget + 40
    frame #14: 0x00007fffab12e928 HIToolbox`IsUserStillTracking(MenuSelectData*, unsigned char*) + 1658
    frame #15: 0x00007fffab255dc4 HIToolbox`TrackMenuCommon(MenuSelectData&, unsigned char*, SelectionData*, MenuResult*, MenuResult*) + 1664
    frame #16: 0x00007fffab13a223 HIToolbox`MenuSelectCore(MenuData*, Point, double, unsigned int, OpaqueMenuRef**, unsigned short*) + 554
    frame #17: 0x00007fffab139f66 HIToolbox`_HandleMenuSelection2 + 460
    frame #18: 0x00007fffa97ee368 AppKit`_NSHandleCarbonMenuEvent + 239
    frame #19: 0x00007fffa9a68702 AppKit`_DPSEventHandledByCarbon + 54
    frame #20: 0x00007fffa9de90c5 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 963
    frame #21: 0x00007fffa96623db AppKit`-[NSApplication run] + 926
    frame #22: 0x00007fffa962ce0e AppKit`NSApplicationMain + 1237
    frame #23: 0x00000001000035fd menutest`main at AppDelegate.swift:13
    frame #24: 0x00007fffc12fc235 libdyld.dylib`start + 1

如您所见,事件并没有通过 Cocoa 事件分派机制分派,因为菜单实际上是 Carbon。 没错,许多应该在过渡到 64 位实际上仍然非常活跃;它们现在只是私有 API。 我们不能在 64 位模式下使用它们,但 Apple 确实可以,而且整个菜单系统仍然是在 Carbon 事件模型之上实现的。因为第三方开发人员可以从头开始重写,比如 Photoshop,但是有人在 1997 年编写的菜单处理代码太有价值了,不能放弃,我相信你同意。

无论如何,我通过调出-[NSCarbonWindow sendEvent:] 做了一个小测试,这是这个回溯中最早的 Objective-C 方法(除了最顶层的东西),看看它是否在子菜单项被调用时被调用点击,它不是。因此,如果我不得不猜测,我会说问题出在 Carbon 事件处理程序上。好吧,这可能在后端有点痛苦,但是嘿,没问题!我们可以通过下降到 Carbon 级别并安装我们自己的 Carbon 事件处理程序来解决这个问题。好吧,卷起​​袖子,我们开始吧——

哦,对了。

我们不能在 64 位模式下使用这些 API。

无论如何,我很遗憾地认为,除了使用讨厌的 hack 来使用像 this guy did 这样的私有 API 并冒着未来被破坏的风险(更不用说被从应用商店)。或者做一些真正疯狂的事情,比如在回溯中对其中一个 C 函数进行猴子补丁,这可能会更糟。不过,这整个问题似乎确实值得 Radar 报告。请file one with Apple 让他们知道这个问题,也许他们会在未来的某个版本中修复它。

编辑:实际上有一个解决方案。由于附加到没有子菜单的菜单项的视图确实会收到您期望的鼠标事件,因此您可以放弃设置 submenu 并让您的视图捕获 mouseEntered:mouseExited: 事件并显示自己菜单,从而模拟子菜单。不是世界上最理想的解决方案,但至少是这样。

【讨论】:

以上是关于具有自定义视图的 NSMenuItem 不接收鼠标事件的主要内容,如果未能解决你的问题,请参考以下文章

NSMenuItem 自定义视图未更新

NSMenuItem、自定义视图和 mouseUp 的奇怪问题:

如何删除自定义视图上方的 NSMenuItem 间隙

单击后从 NSMenuItem 中删除突出显示?

如何在 NSMenuItem 的操作上设置发件人?

MacOS:在NSMenuItem快捷键入或鼠标按下之间进行标识