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

标签 objective-c macos cocoa nsmenu nsmenuitem

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

enter image description here

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

'App Store...' 项目看起来是一个普通的 NSMenuItem。是否有一种简单的方法来实现它,或者是否有任何 API 支持它而无需为菜单项设置自定义 View ?

最佳答案

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

/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Resources/English.lproj/StandardMenus.nib/objects.xib

不幸的是,与 32 位版本相比,64 位 Carbon API 似乎充满了错误和缺失的功能,这使得安装工作绘图处理程序变得更加困难。这是我想出的一个 hacky 版本:

#import <Carbon/Carbon.h>

OSStatus eventHandler(EventHandlerCallRef inHandlerRef, EventRef inEvent, void *inUserData) {
  OSStatus ret = 0;

  if (GetEventClass(inEvent) == kEventClassMenu) {
    if (GetEventKind(inEvent) == kEventMenuDrawItem) {
      // draw the standard menu stuff
      ret = CallNextEventHandler(inHandlerRef, inEvent);

      MenuTrackingData tracking_data;
      GetMenuTrackingData(menuRef, &tracking_data);

      MenuItemIndex item_index;
      GetEventParameter(inEvent, kEventParamMenuItemIndex, typeMenuItemIndex, nil, sizeof(item_index), nil, &item_index);

      if (tracking_data.itemSelected == item_index) {
        HIRect item_rect;
        GetEventParameter(inEvent, kEventParamMenuItemBounds, typeHIRect, nil, sizeof(item_rect), nil, &item_rect);

        CGContextRef context;
        GetEventParameter(inEvent, kEventParamCGContextRef, typeCGContextRef, nil, sizeof(context), nil, &context);

        // first REMOVE a state from the graphics stack, instead of pushing onto the stack
        // this is to remove the clipping and translation values that are completely useless without the context height value
        extern void *CGContextCopyTopGState(CGContextRef);
        void *state = CGContextCopyTopGState(context);

        CGContextRestoreGState(context);

        // draw our content on top of the menu item
        CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 0.5);
        CGContextFillRect(context, CGRectMake(0, item_rect.origin.y - tracking_data.virtualMenuTop, item_rect.size.width, item_rect.size.height));

        // and push a dummy graphics state onto the stack so the calling function can pop it again and be none the wiser
        CGContextSaveGState(context);
        extern void CGContextReplaceTopGState(CGContextRef, void *);
        CGContextReplaceTopGState(context, state);

        extern void CGGStateRelease(void *);
        CGGStateRelease(state);
      }
    }
  }
}

- (void)beginTracking:(NSNotification *)notification {
  // install a Carbon event handler to custom draw in the menu
  if (menuRef == nil) {
    extern MenuRef _NSGetCarbonMenu(NSMenu *);
    extern EventTargetRef GetMenuEventTarget(MenuRef);

    menuRef = _NSGetCarbonMenu(menu);
    if (menuRef == nil) return;

    EventTypeSpec events[1];
    events[0].eventClass = kEventClassMenu;
    events[0].eventKind = kEventMenuDrawItem;

    InstallEventHandler(GetMenuEventTarget(menuRef), NewEventHandlerUPP(&eventHandler), GetEventTypeCount(events), events, nil, nil);
  }

  if (menuRef != nil) {
    // set the kMenuItemAttrCustomDraw attrib on the menu item
    // this attribute is needed in order to receive the kMenuEventDrawItem event in the Carbon event handler
    extern OSStatus ChangeMenuItemAttributes(MenuRef, MenuItemIndex, MenuItemAttributes, MenuItemAttributes);
    ChangeMenuItemAttributes(menuRef, item_index, kMenuItemAttrCustomDraw, 0);
  }
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  menu = [[NSMenu alloc] initWithTitle:@""];

  // register for the BeginTracking notification so we can install our Carbon event handler as soon as the menu is constructed
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(beginTracking:) name:NSMenuDidBeginTrackingNotification object:menu];
}

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

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

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

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

这很古怪和荒谬,但比使用 setView 并重新实现整个菜单项行为要好。 OS X 上的菜单 API 有点一团糟。

关于objective-c - 如何在 NSMenuItem 中绘制内联样式标签(或按钮),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26548393/

相关文章:

iphone - 不时显示完整图像 - iphone

objective-c - 如何在 Mac 命令行应用程序中获取自定义 url?

objective-c - AF网络进度方法

ios - 在 iOS 中的核心数据和 NSFetchedResultsController 中点击我的屏幕之前,reloadData 无法正常工作

objective-c - 如何测试 NSUserDefaults 用户设置中是否存在 BOOL?

ios - [__NSCFString 字符值] : unrecognized selector sent to instance?

cocoa - 在哪里实现不依赖于窗口的菜单项的方法(Cocoa OSX

macos - 我如何找出 macPorts 如何窃取我的端口 :80?

objective-c - 最佳实践委托(delegate)&&数据源模式

objective-c - KVC : How to test for an existing key