Cocoa:如何在 Mail.app 中绘制插入文本?

Posted

技术标签:

【中文标题】Cocoa:如何在 Mail.app 中绘制插入文本?【英文标题】:Cocoa: How to draw inset text as in Mail.app? 【发布时间】:2014-01-05 00:57:42 【问题描述】:

如何在 Cocoa (OS X) 中绘制这种风格的文本?它似乎用于多个 Apple 应用程序,包括 Mail(如上图所示)和 Xcode 侧边栏中的几个地方。我环顾四周,但找不到任何建议如何重现这种特定文本样式的资源。它看起来像一个嵌入的阴影,我的第一个猜测是尝试使用模糊半径设置为负值的 NSShadow,但显然只允许使用正值。有什么想法吗?

【问题讨论】:

你试过NSBackgroundStyleRaised吗?这篇博文:semireg.com/cocoa/2013/06/03/achieving-cocoa-inner-shadow 似乎也很有希望。这个 cocoa-dev 线程也是如此:cocoabuilder.com/archive/cocoa/… 为了记录,我搜索了“cocoa text with inner shadow”。 是的,我确实尝试过 NSBackgroundStyleRaised - 不行。您指向github.com/caylanlarson/texteffect 的博文链接看起来很有希望... 绘制插图意味着你想用 NSView 来做?或者你想返回 NSImage?我想我有一个返回 NSImage 的函数。 NSCell,不过没关系,我可以适应。 【参考方案1】:

我有一些绘制浮雕单元格的代码(我相信最初是由 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 的子类,它做一个内部阴影。【参考方案2】:

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

https://github.com/danieljfarrell/InnerShadowTextFieldCell

根据 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 中更改 text colortext background color 属性,否则您将无法看到阴影。

【讨论】:

【参考方案3】:

从您的屏幕截图中,这看起来像是用内部阴影绘制的文本。因此,使用模糊半径为 0 的标准 NSShadow 方法将不起作用,因为它只会在文本下方/上方绘制阴影。

绘制带有内阴影的文本有两个步骤。

1.获取文字的绘制路径

为了能够在文本字形中绘制阴影,您需要从字符串创建一个贝塞尔路径。 Apple 示例代码 SpeedometerView 具有 a category,它将方法 -bezierWithFont: 添加到 NSString。运行项目看看这个方法是怎么用的。

2。用内部阴影填充路径

在贝塞尔路径下绘制阴影很容易,但在内部绘制阴影并非易事。幸运的是,NSBezierPath+MCAdditions 类别添加了-[NSBezierPath fillWithInnerShadow:] 方法来简化此操作。

【讨论】:

我在下面使用您推荐的 NSString 类别实现了。

以上是关于Cocoa:如何在 Mail.app 中绘制插入文本?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Cocoa 中的应用程序名称中获取 Bundle Identifier?

Cocoa:如何让表格视图在每个单元格中绘制自定义视图

如何在稀疏点之间插入数据以在 R & plotly 中绘制等高线图

如何在Mail.app中运行的applescript中使用钥匙串中的密码?

Cocoa:如何将按钮连接到视图?

在 iOS 8 中,如何通过 Mail.app 实现长滑动删除手势