Как +[NSColor selectedMenuItemColor] волшебным образом рисует градиент?

Я реализую собственное представление NSMenuItem, которое показывает выделение как пользователь наводит на него курсор мыши. Для этого код вызывает NSRectFill после установки [NSColor selectedMenuItemColor] в качестве активного цвета. Однако я заметил, что в результате получается не просто сплошной цвет — вместо этого он фактически рисует градиент. Очень мило, но интересно, как работает это «волшебство» — например, если бы я хотел определить свой собственный цвет, который не просто рисовал сплошным цветом, как бы я это сделал?


person natevw    schedule 26.03.2013    source источник


Ответы (2)


Я не знаю, как это на самом деле работает, но я нашел способ воспроизвести поведение с помощью пользовательских градиентов (или любых других операций рисования). «Хитрость» заключается в использовании CGPatternRef, что позволяет указать функцию обратного вызова для рисования шаблона. Обычно эта функция обратного вызова рисует одну «ячейку» шаблона, но вы можете просто указать очень большой размер шаблона (например, CGFLOAT_MAX), чтобы иметь возможность заполнить всю область за один вызов обратного вызова.

Чтобы продемонстрировать эту технику, вот категория на NSColor, которая позволяет вам создать цвет из NSGradient. Когда вы set выбираете этот цвет, а затем используете его для заполнения области, рисуется градиент (линейный, снизу вверх, но вы можете легко изменить его). Это работает даже для обводки контуров или заполнения непрямоугольных контуров, например [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(0, 0, 100, 100)] fill], потому что NSBezierPath автоматически обрезает рисунок.

//NSColor+Gradient.h
#import <Cocoa/Cocoa.h>

@interface NSColor (Gradient)

+ (NSColor *)my_gradientColorWithGradient:(NSGradient *)gradient;

@end

//NSColor+Gradient.m
#import "NSColor+Gradient.h"
#import <objc/runtime.h>

static void DrawGradientPattern(void * info, CGContextRef context)
{
    NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
    CGRect clipRect = CGContextGetClipBoundingBox(context);
    [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:NO]];
    NSGradient *gradient = (__bridge NSGradient *)info;
    [gradient drawInRect:NSRectFromCGRect(clipRect) angle:90.0];
    [NSGraphicsContext setCurrentContext:currentContext];
}

@implementation NSColor (Gradient)

+ (NSColor *)my_gradientColorWithGradient:(NSGradient *)gradient
{
    CGColorSpaceRef colorSpace = CGColorSpaceCreatePattern(NULL);
    CGPatternCallbacks callbacks;
    callbacks.drawPattern = &DrawGradientPattern;
    callbacks.releaseInfo = NULL;
    CGPatternRef pattern = CGPatternCreate((__bridge void *)(gradient), CGRectMake(0, 0, CGFLOAT_MAX, CGFLOAT_MAX), CGAffineTransformIdentity, CGFLOAT_MAX, CGFLOAT_MAX, kCGPatternTilingConstantSpacing, true, &callbacks);
    const CGFloat components[4] = {1.0, 1.0, 1.0, 1.0};
    CGColorRef cgColor = CGColorCreateWithPattern(colorSpace, pattern, components); 
    CGColorSpaceRelease(colorSpace);
    NSColor *color = [NSColor colorWithCGColor:cgColor];
    objc_setAssociatedObject(color, "gradient", gradient, OBJC_ASSOCIATION_RETAIN);
    return color;
}

@end

Пример использования:

NSArray *colors = @[ [NSColor redColor], [NSColor blueColor] ];
NSGradient *gradient = [[NSGradient alloc] initWithColors:colors];
NSColor *gradientColor = [NSColor my_gradientColorWithGradient:gradient];
[gradientColor set];
NSRectFill(NSMakeRect(0, 0, 100, 100));
[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(100, 0, 100, 100)] fill];

Результат:

Снимок экрана

person omz    schedule 26.03.2013
comment
Потрясающе… да, переход к функциональности CGPattern определенно становится ближе! Единственная недостающая часть — это то, как он обрабатывает частичную перерисовку представления. т.е. вы просто рисуете градиент в текущем клипе, но при редактировании части вида это может не совпадать с ранее нарисованной частью? Похоже, либо ему придется использовать внутреннее знание того, какой вид в данный момент рисует поток, либо, может быть, какая-то магия фазы шаблона? - person natevw; 27.03.2013
comment
Хм, правда. Не уверен, как бы я подошел к этому или если это вообще возможно (используя общедоступные API)... - person omz; 27.03.2013
comment
К сожалению, я не могу протестировать ваш код непосредственно на 10.7, но я подтвердил две вещи: 1) по крайней мере, когда вы оборачиваете свой код для рисования cgColor в текущем графическом порте контекста Cocoa, он соответствует градиенту для dirtyRect, как и опасались, в то время как 2 ) selectedMenuItemColor волшебным образом рисует независимо от прямоугольника клипа или нарисованного прямоугольника. А пока голосую за лучшее предположение! - person natevw; 27.03.2013