29
votes

I'm trying to crate a NSWindow without title bar (NSBorderlessWindowMask) with round corners and a shadow, similar to the below "Welcome to Xcode" window.

Welcome to Xcode

I make a subclass of NSWindow:

@implementation FlatWindow

- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
    self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];

    if ( self )
    {
        [self setOpaque:NO];
        [self setBackgroundColor:[NSColor clearColor]];
        [self setMovableByWindowBackground:TRUE];
        [self setStyleMask:NSBorderlessWindowMask];
        [self setHasShadow:YES];
    }

    return self;
}

- (void) setContentView:(NSView *)aView
{
    aView.wantsLayer            = YES;
    aView.layer.frame           = aView.frame;
    aView.layer.cornerRadius    = 10.0;
    aView.layer.masksToBounds   = YES;

    [super setContentView:aView];
}

@end

And a subclass of NSView:

@implementation ColoredView

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    [[NSColor windowBackgroundColor] set];
    NSRectFill(dirtyRect);
}

@end

This gives me a window without title bar with round corners, but the default shadow on NSWindow is gone. How can I add the default shadow to this window?

Flat window

EDIT1:

NSWindow with NSShadow. This shadow is not shown.

@implementation FlatWindow

- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
    self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];

    if ( self )
    {
        [self setOpaque:NO];
        [self setBackgroundColor:[NSColor clearColor]];
        [self setMovableByWindowBackground:TRUE];
        [self setStyleMask:NSBorderlessWindowMask];
        [self setHasShadow:YES];
    }

    return self;
}

- (void) setContentView:(NSView *)aView
{
    aView.wantsLayer            = YES;
    aView.layer.frame           = aView.frame;
    aView.layer.cornerRadius    = 10.0;
    aView.layer.masksToBounds   = YES;

    NSShadow *dropShadow = [[NSShadow alloc] init];
    [dropShadow setShadowColor:[NSColor blackColor]];
    [dropShadow setShadowBlurRadius:10.0];
    [aView setShadow: dropShadow];

    [super setContentView:aView];
}

@end
11

11 Answers

18
votes

Update

I realised old approach was not able to create precise round corner. So I updated example to make precise round corner.

enter image description here

        window1.backgroundColor             =   NSColor.whiteColor()
        window1.opaque                      =   false
        window1.styleMask                   =   NSResizableWindowMask
                                            |   NSTitledWindowMask
                                            |   NSFullSizeContentViewWindowMask
        window1.movableByWindowBackground   =   true
        window1.titlebarAppearsTransparent  =   true
        window1.titleVisibility             =   .Hidden
        window1.showsToolbarButton          =   false
        window1.standardWindowButton(NSWindowButton.FullScreenButton)?.hidden   =   true
        window1.standardWindowButton(NSWindowButton.MiniaturizeButton)?.hidden  =   true
        window1.standardWindowButton(NSWindowButton.CloseButton)?.hidden        =   true
        window1.standardWindowButton(NSWindowButton.ZoomButton)?.hidden         =   true

        window1.setFrame(CGRect(x: 400, y: 0, width: 400, height: 500), display: true)
        window1.makeKeyAndOrderFront(self)

Here's full working example.


Oudated

Special treatment is not required at least in OS X 10.10.

import Cocoa

class ExampleApplicationController: NSObject, NSApplicationDelegate {
    class ExampleController {

        let window1 =   NSWindow()
        let view1   =   NSView()

        init(){
            window1.setFrame(CGRect(x: 400, y: 0, width: 400, height: 500), display: true)
            window1.contentView                 =   view1

            window1.backgroundColor             =   NSColor.clearColor()
            window1.opaque                      =   false
            window1.styleMask                   =   NSBorderlessWindowMask | NSResizableWindowMask
            window1.movableByWindowBackground   =   true
            window1.makeKeyAndOrderFront(self)

            view1.wantsLayer                =   true
            view1.layer!.cornerRadius       =   10
            view1.layer!.backgroundColor    =   NSColor.whiteColor().CGColor

            /// :ref:   http://stackoverflow.com/questions/19940019/nswindow-with-round-corners-and-shadow/27613308#21247949
            window1.invalidateShadow()  //  This manual invalidation is REQUIRED because shadow generation is an expensive operation.
        }
    }

    let example1    =   ExampleController()
}

You can download a working example from here.

3
votes

There is a sample application on the Apple Developer Site that may help you with that. This sample demonstrates how to create windows with custom shapes, no title bar, and transparent content. It also shows how to change the shape of the window and recalculate the drop shadow around the window border.

3
votes

I just created my own version of the Xcode splash screen for my application, including the recent files list view and all four corners are rounded:

http://www.fizzypopstudios.com/splash.png

The easiest way I found to do this was to create a window using the interface builder. I made the window 800x470 pixles, then I unchecked all the options except "Shadow" and "Restorable". This left me with a blank slate with which to create my splash screen.

In the initWithContentRect:styleMask:backing:defer: method for the splash window I also set the following properties:

self.opaque = NO
self.backgroundColor = [NSColor clearColor]
self.movableByWindowBackground = YES

If you displayed the window at this point you would have no background, and the window shadow would automatically be set to be behind any non-transparent controls in the window.

When I draw my window background I fill the left 500 pixels with light gray and the remaining 300 pixels on the right with white. If displayed the window would have no rounded corners at this point.

I then use [NSColor clearColor] to notch squares out all 4 corners of the window (the size of the square is the radius of the rounded corner we will draw next). Then using Bezier Paths I draw in a rounded corner into the notches I just cut out.

I tried to do this by creating a bezier path that when filled with [NSColor clearColor] would also result in a round corner, however, for some reason the path would not fill with clear, though it would fill with other colors.

Now, if you render the window you will have rounded corners, however, if you drop a table view into the right side of the window the corners will become square again. The easy solution there is to set the NSScrollView to not draw a background, and then to set the nested NSTableView background to transparent.

As we are drawing the background behind the table we don't really need the table or scroll view drawing a background, and this then preserves the rounded corners.

If you have any other controls that go near the 4 corners just keep them indented enough to not be in the rounded area. An example of this is I have a button in the lower left of my window, however, since the button has transparency the rounded corner is not removed.

One other consideration is you may want to provide canBecomeKeyWindow and canBecomeMainWindow methods in your window subclass to return YES as the default for these types of windows is NO.

I hope this info helps you create your window, I saw your question a while ago and thought I would come back and let you know how I created my window in case it could help you! :)

3
votes

Objective C example of @Eonil's answer:

[window setBackgroundColor:[NSColor whiteColor]];
[window setOpaque:NO];
[window setStyleMask:NSResizableWindowMask | NSTitledWindowMask | NSFullSizeContentViewWindowMask];
[window setMovableByWindowBackground:YES];
[window setTitlebarAppearsTransparent:YES];
[window setTitleVisibility:NSWindowTitleHidden];
[window setShowsToolbarButton:NO];
[window standardWindowButton:NSWindowFullScreenButton].hidden = YES;
[window standardWindowButton:NSWindowMiniaturizeButton].hidden = YES;
[window standardWindowButton:NSWindowCloseButton].hidden = YES;
[window standardWindowButton:NSWindowZoomButton].hidden = YES;
[window makeKeyWindow];
2
votes

As the Apple Developer Documentation perfectly points out:

When adding shadows to a layer, the shadow is part of the layer’s content but actually extends outside the layer’s bounds rectangle. As a result, if you enable the masksToBounds property for the layer, the shadow effect is clipped around the edges. If your layer contains any transparent content, this can cause an odd effect where the portion of the shadow directly under your layer is still visible but the part extending beyond your layer is not. If you want a shadow but also want to use bounds masking, you use two layers instead of one. Apply the mask to the layer containing your content and then embed that layer inside a second layer of the exact same size that has the shadow effect enabled.

Full Article: https://developer.apple.com/library/mac/documentation/cocoa/conceptual/coreanimation_guide/SettingUpLayerObjects/SettingUpLayerObjects.html#//apple_ref/doc/uid/TP40004514-CH13-SW12

So in fact you have to work with two views/layers:

- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];

if ( self )
{
    [self setOpaque:NO];
    [self setBackgroundColor:[NSColor clearColor]];
    [self setMovableByWindowBackground:TRUE];
    [self setStyleMask:NSBorderlessWindowMask];
    [self setHasShadow:YES];
}

return self;
}

- (BOOL)canBecomeKeyWindow {
return YES;
}

-(BOOL)canBecomeMainWindow {
return YES;
}

- (void) setContentView:(NSView *)aView {

NSView *backView = [[NSView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
backView.wantsLayer             = YES;
backView.layer.masksToBounds    = NO;
backView.layer.shadowColor      = [NSColor shadowColor].CGColor;
backView.layer.shadowOpacity    = 0.5;
backView.layer.shadowOffset     = CGSizeMake(0, -3);
backView.layer.shadowRadius     = 5.0;
backView.layer.shouldRasterize  = YES;


NSView *frontView = [aView initWithFrame:CGRectMake(backView.frame.origin.x + 15, backView.frame.origin.y + 15, backView.frame.size.width - 30, backView.frame.size.height - 30)];
[backView addSubview: frontView];
frontView.layer.cornerRadius    = 8;
frontView.layer.masksToBounds   = YES;
frontView.layer.borderColor     = [[NSColor darkGrayColor] CGColor];
frontView.layer.borderWidth     = 0.5;

[super setContentView:backView];

}

Have a closer look at the "initWithFrame" part of the code. Best greetings from Tyrol, Austria!

1
votes

You have two options:

  1. Use the layer's shadow properties.
  2. Draw the shadow yourself in your drawRect routine. For this to work don't set the rounded corners but use an inset path to draw the rounded rectangle and its shadow.

Code:

@interface RoundedOuterShadowView : NSView {
}

@end

@implementation RoundedOuterShadowView

- (id)initWithFrame: (NSRect)frameRect
{
    self = [super initWithFrame: frameRect];
    if (self != nil) {
    }

    return self;
}

// Shared objects.
static NSShadow *borderShadow = nil;

- (void)drawRect: (NSRect)rect
{
    [NSGraphicsContext saveGraphicsState];

    // Initialize shared objects.
    if (borderShadow == nil) {
        borderShadow = [[NSShadow alloc] initWithColor: [NSColor colorWithDeviceWhite: 0 alpha: 0.5]
                                                offset: NSMakeSize(1, -1)
                                            blurRadius: 5.0];
    }

    // Outer bounds with shadow.
    NSRect bounds = [self bounds];
    bounds.size.width -= 20;
    bounds.size.height -= 20;
    bounds.origin.x += 10;
    bounds.origin.y += 10;

    NSBezierPath *borderPath = [NSBezierPath bezierPathWithRoundedRect: bounds xRadius: 5 yRadius: 5];
    [borderShadow set];
    [[NSColor whiteColor] set];
    [borderPath fill];

    [NSGraphicsContext restoreGraphicsState];
}

@end
1
votes

You are struggling with the same issue I had, but the good news is that I have found a way pretty much easy to make it work.

So, to the concepts, if you look at Xcode's welcome screen, it pretty much looks like a regular window but has no title bar (does it?).

Window Setup

What I've done is I've taken a regular window and with it selected, I've gone to the Atribute Inspector and deactivated the "Close", "Minimize" and "Resize" buttons which are the three buttons at the title bar.

So you get a "naked" window, but still has the title bar. What you can do is to add the following code to your awakeFromNib delegate:

[self.window setTitle:@""];

Assuming your window is declared like in the header file:

@property (assign) IBOutlet NSWindow *window;

Now you have a completely naked title bar and what you can do is some final tweaking:

Color

You can do this in the awakeFromNib delegate as well:

[self.window setBackgroundColor: [NSColor colorWithCalibratedWhite:0.97 alpha:1.0]];

Close Button

I've used the following method: Adding a button or view to the NSWindow title bar

So I can just use (I've done that also in the awakeFromNib delegate):

//Creating the button:
NSButton *closeButton = [[NSButton alloc] initWithFrame:NSMakeRect(0,0,12,12)];
NSButtonCell *closeButtonCell = [closeButton cell];

[closeButton setBezelStyle:NSCircularBezelStyle];
[closeButton setTitle:@""];
[closeButton setBordered:NO];    
[closeButton setImage:[NSImage imageNamed:NSImageNameStopProgressFreestandingTemplate]];
[closeButtonCell setImageScaling:NSImageScaleProportionallyDown];
[closeButtonCell setBackgroundColor:[NSColor clearColor]];
[closeButton setAlphaValue:0.5];

//Calling the button:
[self.window addViewToTitleBar:closeButton atXPosition:8];

Now it should look pretty much close to the xcode's welcome screen, now if you want, you can also program the hover effects, which should be pretty easy.

1
votes

Here's a solution that can be done just with Interface Builder and two lines of extra code:

Create a NIB file in Interface Builder containing your window.
Configure it as shown below:

NSWindow without any decorations

Add a box (NSBox) object to the window content view in Interface Builder and make it fill the entire content view. Set border style, border thickness, border color, fill color and corner radius of the box as desired:

NSBox with default values

Add all your other UI content to the box, treat is as if it was your window content view.

In your code, create a subclass of NSWindowController which loads this NIB file and thus becomes the owner. Either in -awakeFromNib or -windowDidLoad, add the following code:

    self.window.opaque = NO;
    self.window.backgroundColor = NSColor.clearColor;

That's all folks.
No layers, no custom drawing code, no NSBezierPath, no NSShadow.
Verified to work correctly on all systems from macOS 10.9 to 10.15

0
votes

I ran across this in a previous project: layer-backed views do not produce a shadow. Set your window's content view to a "regular" (non-layer-backed) view and it will have a shadow.

0
votes

try this:

- (id)initWithContentRect:(NSRect)contentRect
                styleMask:(NSUInteger)windowStyle   
                  backing:(NSBackingStoreType)bufferingType
                    defer:(BOOL)deferCreation
{
    self = [super
            initWithContentRect:contentRect
            styleMask:NSBorderlessWindowMask | NSResizableWindowMask
            backing:bufferingType
            defer:deferCreation];
    if (self)
    {
        [self setOpaque:NO];
       [self setBackgroundColor:[NSColor clearColor]];            

    }

      [self setHasShadow:YES];


    [self setMovableByWindowBackground:YES];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(didBecomeKey:) name:NSWindowDidBecomeKeyNotification object:self];

    return self;
}

-(void)didBecomeKey:(NSNotification *)notify{
    [self performSelector:@selector(invalidateShadow) withObject:nil afterDelay:.1];
}
0
votes

In your view's -drawRect:, after the drawing, invoke [[self window] invalidateShadow];.