Cocoa:如何像Mail.app一样绘制内嵌文本?

3
我该如何在Cocoa(OS X)中绘制这种文本样式?它似乎在几个苹果应用程序中使用,包括Mail(如上图所示)和Xcode侧边栏的几个位置。我已经找了一圈,但没有找到任何资源建议如何复制这种特定的文本样式。它看起来像是一个内嵌阴影,我的第一个猜测是尝试使用模糊半径设为负值的NSShadow,但显然只允许使用正值。有什么想法吗?

2
你尝试过 NSBackgroundStyleRaised 吗?这篇博客文章:http://www.semireg.com/cocoa/2013/06/03/achieving-cocoa-inner-shadow/ 似乎也很有希望。还有这个 cocoa-dev 的主题:http://www.cocoabuilder.com/archive/cocoa/306747-drawing-text-like-lion-mail.html。 - jscs
1
记录一下,我搜索了“cocoa text with inner shadow”这个关键词。 - jscs
是的,我确实尝试了NSBackgroundStyleRaised - 但不行。您的博客文章链接 https://github.com/caylanlarson/texteffect 看起来很有前途... - Jake Petroules
绘制插入文本是指您想使用NSView进行操作吗?还是您想返回NSImage?我认为我有一个可以返回NSImage的函数。 - El Tomato
NSCell,但这并不重要,我可以适应它。 - Jake Petroules
3个回答

4

我有一段代码,可以绘制浮雕式单元格(最初由Jonathon Mah编写)。它可能不能完全符合您的要求,但它会给您一个开始的地方:

@implementation DMEmbossedTextFieldCell

#pragma mark NSCell

- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView;
{
    /* This method copies the three-layer method used by Safari's error page. That's accessible by forcing an
     * error (e.g. visiting <foo://>) and opening the web inspector. */

    // I tried to use NSBaselineOffsetAttributeName instead of shifting the frame, but that didn't seem to work
    const NSRect onePixelUpFrame = NSOffsetRect(cellFrame, 0.0, [NSGraphicsContext currentContext].isFlipped ? -1.0 : 1.0);
    const NSRange fullRange = NSMakeRange(0, self.attributedStringValue.length);

    NSMutableAttributedString *scratchString = [self.attributedStringValue mutableCopy];

    BOOL overDark = (self.backgroundStyle == NSBackgroundStyleDark);
    CGFloat (^whenLight)(CGFloat) = ^(CGFloat b) { return overDark ? 1.0 - b : b; };

    // Layer 1
    [scratchString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithCalibratedWhite:whenLight(1.0) alpha:1.0] range:fullRange];
    [scratchString drawInRect:cellFrame];

    // Layer 2
    BOOL useGradient = NO; // Safari 5.2 preview has switched to a lighter, solid color look for the detail text. Since we use the same class, use bold-ness to decide
    if (self.attributedStringValue.length > 0) {
        NSFont *font = [self.attributedStringValue attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
        if ([[NSFontManager sharedFontManager] traitsOfFont:font] & NSBoldFontMask)
            useGradient = YES;
    }

    NSColor *solidShade = [NSColor colorWithCalibratedHue:200/360.0 saturation:0.03 brightness:whenLight(0.41) alpha:1.0];
    [scratchString addAttribute:NSForegroundColorAttributeName value:solidShade range:fullRange];
    [scratchString drawInRect:onePixelUpFrame];

    // Layer 3 (Safari uses alpha of 0.25)
    [scratchString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithCalibratedWhite:whenLight(1.0) alpha:0.25] range:fullRange];
    [scratchString drawInRect:cellFrame];
}

@end

我将以下代码实现为NSTextFieldCell的子类,它可以产生内部阴影。 - Daniel Farrell

3
请不要选择这个作为答案,我只是出于好玩实现了上述建议,并将其放在这里,因为它可能对未来的某些人有用!

https://github.com/danieljfarrell/InnerShadowTextFieldCell

Subclass of NSTextFieldCell which uses an inner shadow.

以下是 indragiewil-shipley 的建议,这里提供了一个 NSTextFieldCell 的子类,可以用内部阴影绘制文本。
头文件如下:
// InnerShadowTextFieldCell.h
#import <Cocoa/Cocoa.h>
@interface InnerShadowTextFieldCell : NSTextFieldCell
@property (strong) NSShadow *innerShadow;
@end

现在是实现文件,

// InnerShadowTextFieldCell.m
#import "InnerShadowTextFieldCell.h"
// This class needs the NSString category -bezierWithFont: from,
// https://developer.apple.com/library/mac/samplecode/SpeedometerView/Listings/SpeedyCategories_m.html

@implementation InnerShadowTextFieldCell


- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {

    //// Shadow Declarations
    if (_innerShadow == nil) {
        /* Inner shadow has not been set, override here with default shadow.
           You may or may not want this behaviour. */
        _innerShadow = [[NSShadow alloc] init];
        [_innerShadow setShadowColor: [NSColor darkGrayColor]];
        [_innerShadow setShadowOffset: NSMakeSize(0.1, 0.1)];
        /* Trying to find a default shadow radius which will look good for
           a label of any size, let's get a rough estimate based on the
           hypotenuse of the cell frame. */
        [_innerShadow setShadowBlurRadius: 0.0075 * hypot(NSWidth(cellFrame), NSHeight(cellFrame)) ];
    }

    /* Because we are using the -bezierWithFont: we get a slightly 
       different path than had we used the superclass to drawn the 
       text path. This means that the background colour and text 
       colour looks odd if we use call the superclass's,
            -drawInteriorWithFrame:inView: method let's do that 
       drawing here. Not making the call to super might cause 
       problems for general use (?) but for a simple label is seems 
       to work OK */
    [self.backgroundColor setFill];
    NSRectFill(cellFrame);

    NSBezierPath *bezierPath = [self.title bezierWithFont:self.font];
    [self.textColor setFill];
    [bezierPath fill];


    /* The following is inner shadow drawing method is taken from Paint Code */

    ////// Bezier Inner Shadow
    NSShadow *shadow = _innerShadow;
    NSRect bezierBorderRect = NSInsetRect([bezierPath bounds], -shadow.shadowBlurRadius, -shadow.shadowBlurRadius);
    bezierBorderRect = NSOffsetRect(bezierBorderRect, -shadow.shadowOffset.width, shadow.shadowOffset.height);
    bezierBorderRect = NSInsetRect(NSUnionRect(bezierBorderRect, [bezierPath bounds]), -1, -1);

    NSBezierPath* bezierNegativePath = [NSBezierPath bezierPathWithRect: bezierBorderRect];
    [bezierNegativePath appendBezierPath: bezierPath];
    [bezierNegativePath setWindingRule: NSEvenOddWindingRule];

    [NSGraphicsContext saveGraphicsState];
    {
        NSShadow* shadowWithOffset = [shadow copy];
        CGFloat xOffset = shadowWithOffset.shadowOffset.width + round(bezierBorderRect.size.width);
        CGFloat yOffset = shadowWithOffset.shadowOffset.height;
        shadowWithOffset.shadowOffset = NSMakeSize(xOffset + copysign(0.0, xOffset), yOffset + copysign(0.1, yOffset));
        [shadowWithOffset set];
        [[NSColor grayColor] setFill];
        [bezierPath addClip];
        NSAffineTransform* transform = [NSAffineTransform transform];
        [transform translateXBy: -round(bezierBorderRect.size.width) yBy: 0];
        [[transform transformBezierPath: bezierNegativePath] fill];
    }
    [NSGraphicsContext restoreGraphicsState];


}
@end

这可能可以更加健壮,但对于绘制静态标签来说似乎已经足够了。
在Interface Builder中确保更改文本颜色和文本背景颜色属性,否则您将无法看到阴影。

2
根据您的截图,那看起来像是带内阴影的文本。因此,标准的NSShadow方法使用模糊半径为0不起作用,因为那只会在文本下方/上方绘制阴影。
有两个步骤来绘制带有内阴影的文本。
1. 获取文本的绘制路径
为了能够在文本字形内部绘制阴影,您需要从字符串创建一个贝塞尔路径。苹果的示例代码SpeedometerView有一个类别,添加了方法-bezierWithFont:NSString中。运行项目以查看如何使用此方法。
2. 用内阴影填充路径
在贝塞尔路径下绘制阴影很容易,但在路径内部绘制阴影并不简单。幸运的是,NSBezierPath+MCAdditions类别添加了-[NSBezierPath fillWithInnerShadow:]方法,使此过程变得简单。

我使用你推荐的NSString类别实现了下面的内容。 - Daniel Farrell

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