+[NSColor selectedMenuItemColor] 如何神奇地绘制渐变?

4

我正在实现一个自定义的NSMenuItem视图,当用户将鼠标悬停在其上时显示高亮。为此,代码在设置[NSColor selectedMenuItemColor]为活动颜色后调用了NSRectFill。然而,我注意到结果并不仅仅是一个纯色,它实际上绘制了一个渐变。非常好,但我想知道这个“魔法”是如何工作的——也就是说,如果我想定义自己的颜色而不只是绘制纯色,我该怎么做?

2个回答

3

我不知道这个实际上是如何工作的,但我找到了一种用自定义渐变(或任何其他绘图操作)复制该行为的方法。"诀窍"在于使用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];

Result:

Screenshot


太棒了...是的,转向CGPattern功能确实更接近了!那么唯一缺失的部分就是它如何处理视图的部分重绘。也就是说,你只是在当前剪辑中绘制渐变,但当“修整”视图的一部分时,这可能与先前绘制的部分不对齐?看起来它要么必须使用线程当前正在绘制的视图的内部知识,要么可能需要一些模式相位魔法? - natevw
嗯,确实。不确定我该如何处理这个问题,或者是否可能(使用公共API)... - omz
很遗憾无法直接在10.7上测试您的代码,但我已确认两件事:1)至少在将您的代码包装以绘制cgColor到当前Cocoa上下文的graphicsPort时,它会适应dirtyRect所担心的渐变,而2)selectedMenuItemColor会“神奇地”绘制,无论剪辑矩形或绘制矩形如何。在此期间为最佳猜测点赞! - natevw

0

我最好的猜测是它被定义为某种图案图像,但这并不能完全回答我的问题,因为看起来这些图案通常会被平铺而不是拉伸。

这得到了一位苹果工程师在cocoa-dev上的帖子的证实:

[[NSColor selectedMenuItemColor] set]; NSRectFill(someRect);

这能够工作是因为selectedMenuItemColor是一个图案,恰好绘制了一个渐变。你可以用图案轻松地绘制几乎任何东西[...]。

他没有详细说明如何拉伸绘制这些图案,就像突出显示的菜单项背景一样。该线程中的另一篇帖子声称这是绘图代码中的“特殊情况”,但他可能只是在猜测。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接