如何在NSMenuItem中绘制内联样式标签(或按钮)

Jam*_*hen 5 macos cocoa objective-c nsmenu nsmenuitem

当App Store有更新时,它会在菜单项中显示内联样式元素,如下面屏幕截图中的"1 new":

在此输入图像描述

另一个我们可以看到这种菜单的地方是10.10 Yosemite的分享菜单.当您安装添加新共享扩展程序的任何应用时,共享菜单中的"更多"项目将显示"N new",就像应用商店菜单一样.

'App Store ...'项看起来很正常NSMenuItem.是否有一种简单的方法来实现它,或者是否有任何支持它的API而没有为菜单项设置自定义视图?

Bon*_*uin 5

“Cocoa”NSMenus 实际上完全基于 Carbon 构建,因此虽然 Cocoa API 没有公开太多功能,但您可以深入研究 Carbon-land 并获得更多功能。这就是 Apple 所做的,无论如何 \xe2\x80\x93 Apple 菜单项是从 的子类IBCarbonMenuItem,如下所示:

\n\n
/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Resources/English.lproj/StandardMenus.nib/objects.xib\n
Run Code Online (Sandbox Code Playgroud)\n\n

不幸的是,64 位 Carbon API 似乎充满了错误和缺失的功能,这使得安装有效的绘制处理程序比 32 位版本困难得多。这是我想出的一个 hacky 版本:

\n\n
#import <Carbon/Carbon.h>\n\nOSStatus eventHandler(EventHandlerCallRef inHandlerRef, EventRef inEvent, void *inUserData) {\n  OSStatus ret = 0;\n\n  if (GetEventClass(inEvent) == kEventClassMenu) {\n    if (GetEventKind(inEvent) == kEventMenuDrawItem) {\n      // draw the standard menu stuff\n      ret = CallNextEventHandler(inHandlerRef, inEvent);\n\n      MenuTrackingData tracking_data;\n      GetMenuTrackingData(menuRef, &tracking_data);\n\n      MenuItemIndex item_index;\n      GetEventParameter(inEvent, kEventParamMenuItemIndex, typeMenuItemIndex, nil, sizeof(item_index), nil, &item_index);\n\n      if (tracking_data.itemSelected == item_index) {\n        HIRect item_rect;\n        GetEventParameter(inEvent, kEventParamMenuItemBounds, typeHIRect, nil, sizeof(item_rect), nil, &item_rect);\n\n        CGContextRef context;\n        GetEventParameter(inEvent, kEventParamCGContextRef, typeCGContextRef, nil, sizeof(context), nil, &context);\n\n        // first REMOVE a state from the graphics stack, instead of pushing onto the stack\n        // this is to remove the clipping and translation values that are completely useless without the context height value\n        extern void *CGContextCopyTopGState(CGContextRef);\n        void *state = CGContextCopyTopGState(context);\n\n        CGContextRestoreGState(context);\n\n        // draw our content on top of the menu item\n        CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 0.5);\n        CGContextFillRect(context, CGRectMake(0, item_rect.origin.y - tracking_data.virtualMenuTop, item_rect.size.width, item_rect.size.height));\n\n        // and push a dummy graphics state onto the stack so the calling function can pop it again and be none the wiser\n        CGContextSaveGState(context);\n        extern void CGContextReplaceTopGState(CGContextRef, void *);\n        CGContextReplaceTopGState(context, state);\n\n        extern void CGGStateRelease(void *);\n        CGGStateRelease(state);\n      }\n    }\n  }\n}\n\n- (void)beginTracking:(NSNotification *)notification {\n  // install a Carbon event handler to custom draw in the menu\n  if (menuRef == nil) {\n    extern MenuRef _NSGetCarbonMenu(NSMenu *);\n    extern EventTargetRef GetMenuEventTarget(MenuRef);\n\n    menuRef = _NSGetCarbonMenu(menu);\n    if (menuRef == nil) return;\n\n    EventTypeSpec events[1];\n    events[0].eventClass = kEventClassMenu;\n    events[0].eventKind = kEventMenuDrawItem;\n\n    InstallEventHandler(GetMenuEventTarget(menuRef), NewEventHandlerUPP(&eventHandler), GetEventTypeCount(events), events, nil, nil);\n  }\n\n  if (menuRef != nil) {\n    // set the kMenuItemAttrCustomDraw attrib on the menu item\n    // this attribute is needed in order to receive the kMenuEventDrawItem event in the Carbon event handler\n    extern OSStatus ChangeMenuItemAttributes(MenuRef, MenuItemIndex, MenuItemAttributes, MenuItemAttributes);\n    ChangeMenuItemAttributes(menuRef, item_index, kMenuItemAttrCustomDraw, 0);\n  }\n}\n\n- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {\n  menu = [[NSMenu alloc] initWithTitle:@""];\n\n  // register for the BeginTracking notification so we can install our Carbon event handler as soon as the menu is constructed\n  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(beginTracking:) name:NSMenuDidBeginTrackingNotification object:menu];\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

首先,它注册 BeginTracking 通知,因为_NSGetCarbonMenu仅在构建菜单后返回有效句柄,并且在绘制菜单之前调用 BeginTracking。

\n\n

然后它使用通知回调来获取 Carbon MenuRef 并将标准 Carbon 事件处理程序附加到菜单。

\n\n

通常我们可以简单地获取kEventParamMenuContextHeight事件参数并翻转 CGContextRef 并开始绘制,但该参数仅在 32 位模式下可用。Apple 的文档建议在该值不可用时使用当前端口的高度,但这也仅在 32 位模式下可用。

\n\n

所以既然给我们的图形状态没有用,就把它从堆栈中弹出并使用之前的图形状态。事实证明,这个新状态被转换为菜单的虚拟顶部,可以使用 检索该菜单GetMenuTrackingData.virtualMenuTop。该kEventParamVirtualMenuTop值在 64 位模式下也不正确,因此必须使用GetMenuTrackingData.

\n\n

这是很hacky和荒谬的,但它比使用setView并重新实现整个菜单项行为要好。OS X 上的菜单 API 有点混乱

\n