»
May 06, 2010
»

Drawing Custom NSButton in Cocoa

In my spare time I’m designing my first “real” Cocoa appplication and I’ve decided to be rather open about this app and it’s implementation details prior and after the release.

I’m not going to disclose anything about the app just yet. Well, maybe only that it will come with open-source cloud component deployable (or automatically deployed) to AppEngine.

While I’m still siting in Photoshop trying to come up with interface design, I wanted to create actual clickable buttons from mockup. Here is more or less step-by-step guide of drawing them.

But now about NSButton custom drawing.

buttons.png

In Cocoa NSButton is subclass of NSControl which are drawn using NSCells. So we need a new NSButtonCell subclass to do our drawing:

#import "AMDarkButtonCell.h"

@implementation AMDarkButtonCell

- (void)drawImage:(NSImage*)image 
        withFrame:(NSRect)frame 
           inView:(NSView*)controlView
{
}

- (NSRect)drawTitle:(NSAttributedString*)title 
          withFrame:(NSRect)frame 
             inView:(NSView*)controlView
{
}

- (void)drawBezelWithFrame:(NSRect)frame 
                    inView:(NSView *)controlView
{
}

@end

My not so great custom button consists of only of image and bezel so I’m not going to override -drawTitle:withFrame:inView:.

Lets start with bezel. It’s composed from:

  • Outer gradient stroke
  • Background gradient
  • Border dark stroke
  • Inner light stroke

Cocoa provides:

  • NSBezierPath for drawing, stroking and clipping-to rounded rectangles
  • NSGradient for drawing multiple point gradients

That’s basically all we need to draw whole background composite.

- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView
{
  NSGraphicsContext *ctx = [NSGraphicsContext currentContext];

  CGFloat roundedRadius = 3.0f;

  // Outer stroke (drawn as gradient)

  [ctx saveGraphicsState];
  NSBezierPath *outerClip = [NSBezierPath bezierPathWithRoundedRect:frame 
                                                            xRadius:roundedRadius 
                                                            yRadius:roundedRadius];
  [outerClip setClip];

  NSGradient *outerGradient = [[NSGradient alloc] initWithColorsAndLocations:
                               [NSColor colorWithDeviceWhite:0.20f alpha:1.0f], 0.0f, 
                               [NSColor colorWithDeviceWhite:0.21f alpha:1.0f], 1.0f, 
                               nil];

  [outerGradient drawInRect:[outerClip bounds] angle:90.0f];
  [outerGradient release];
  [ctx restoreGraphicsState];
 
  // Background gradient

  [ctx saveGraphicsState];
  NSBezierPath *backgroundPath = 
    [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 2.0f, 2.0f) 
                                    xRadius:roundedRadius 
                                    yRadius:roundedRadius];
  [backgroundPath setClip];

  NSGradient *backgroundGradient = [[NSGradient alloc] initWithColorsAndLocations:
                                    [NSColor colorWithDeviceWhite:0.17f alpha:1.0f], 0.0f, 
                                    [NSColor colorWithDeviceWhite:0.20f alpha:1.0f], 0.12f, 
                                    [NSColor colorWithDeviceWhite:0.27f alpha:1.0f], 0.5f, 
                                    [NSColor colorWithDeviceWhite:0.30f alpha:1.0f], 0.5f, 
                                    [NSColor colorWithDeviceWhite:0.42f alpha:1.0f], 0.98f, 
                                    [NSColor colorWithDeviceWhite:0.50f alpha:1.0f], 1.0f, 
                                    nil];

  [backgroundGradient drawInRect:[backgroundPath bounds] angle:270.0f];
  [backgroundGradient release];
  [ctx restoreGraphicsState];

  // Dark stroke

  [ctx saveGraphicsState];
  [[NSColor colorWithDeviceWhite:0.12f alpha:1.0f] setStroke];
  [[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 1.5f, 1.5f) 
                                   xRadius:roundedRadius 
                                   yRadius:roundedRadius] stroke];
  [ctx restoreGraphicsState];

  // Inner light stroke

  [ctx saveGraphicsState];
  [[NSColor colorWithDeviceWhite:1.0f alpha:0.05f] setStroke];
  [[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 2.5f, 2.5f) 
                                   xRadius:roundedRadius 
                                   yRadius:roundedRadius] stroke];
  [ctx restoreGraphicsState];        

  // Draw darker overlay if button is pressed

  if([self isHighlighted]) {
    [ctx saveGraphicsState];
    [[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 2.0f, 2.0f) 
                                     xRadius:roundedRadius 
                                     yRadius:roundedRadius] setClip];
    [[NSColor colorWithCalibratedWhite:0.0f alpha:0.35] setFill];
    NSRectFillUsingOperation(frame, NSCompositeSourceOver);
    [ctx restoreGraphicsState];
  }
}

To draw image I’ve chosen to use it only as a mask for 2 draw operations:

  • Image as solid color
  • It’s shadow as solid color
- (void)drawImage:(NSImage*)image withFrame:(NSRect)frame inView:(NSView*)controlView
{
  NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
  CGContextRef contextRef = [ctx graphicsPort];
  
  NSData *data = [image TIFFRepresentation]; // open for suggestions
  CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)data, NULL);
  if(source) {
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, NULL);
    CFRelease(source);
    
    // Draw shadow 1px below image
    
    CGContextSaveGState(contextRef);
    {
      NSRect rect = NSOffsetRect(frame, 0.0f, 1.0f);
      CGFloat white = [self isHighlighted] ? 0.2f : 0.35f;
      CGContextClipToMask(contextRef, NSRectToCGRect(rect), imageRef);
      [[NSColor colorWithDeviceWhite:white alpha:1.0f] setFill];
      NSRectFill(rect);
    } 
    CGContextRestoreGState(contextRef);
    
    // Draw image
    
    CGContextSaveGState(contextRef);
    {
      NSRect rect = frame;
      CGContextClipToMask(contextRef, NSRectToCGRect(rect), imageRef);
      [[NSColor colorWithDeviceWhite:0.1f alpha:1.0f] setFill];
      NSRectFill(rect);
    } 
    CGContextRestoreGState(contextRef);        
    
    CFRelease(imageRef);
  }
}

Ok, now we have drawing code in place, lets move to Interface Builder and wire up this custom NSButtonCell for buttons which should be drawn with this style.

Add some buttons:

button_window.png

Set Image attribute in “Button Attributes” panel:

button_attrs.png

Click one more time on button in window to select it’s Cell Identity:

cell_window.png

And set cell Class to our custom implementation:

cell_attrs.png

That’s it.

Download example Xcode project (64Kb)

 
Internet Explorer 6
Are you serious?