«Какао» NSMenus на самом деле полностью построен на углероде, поэтому, поскольку API-интерфейсы Cocoa не предоставляют много функциональности, вы можете окунуться в углеродную землю и получить доступ к гораздо большей мощности. Это то, что Apple, делает, во всяком случае - пункты меню Apple являются подклассами IBCarbonMenuItem
, как можно видеть здесь:
/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Resources/English.lproj/StandardMenus.nib/objects.xib
К сожалению, 64-битные интерфейсы углерода, кажется, пронизана ошибок и недостающих функций, что делает его гораздо сложнее установить рабочий обработчик, чем по сравнению с 32-разрядной версией. Вот 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 и переопределение всего поведения элемента меню. API-интерфейсы меню на OS X - это бит беспорядка.
+1. Также ища ответ. Похоже, вы можете настроить NSView для отображения вместо обычного названия NSMenuItem. Но это не так, как я бы назвал «легким». – UJey
Любое повезло с этим? – mileusna
@mileusna Я еще не пробовал решение BonzaiThePenguin из ответа, которое может работать хорошо. –