如何使用NSVisualEffectView创建一个平滑,圆润,类似体积的OS X窗口?

时间:2014-10-22 22:55:49

标签: objective-c macos cocoa nswindow nsvisualeffectview

我正在尝试创建一个类似于Volume OS X窗口的窗口:
enter image description here

为了做到这一点,我有自己的NSWindow(使用自定义子类),它是透明的/无标题栏/无阴影,在其contentView中有NSVisualEffectView 。这是我的子类的代码,使内容视图成为圆形:

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

   [super setContentView:aView];
}


这就是结果(正如你所看到的,角落是颗粒状的,OS X更平滑):
enter image description here
关于如何使角落更平滑的任何想法?感谢

6 个答案:

答案 0 :(得分:22)

OS X El Capitan的更新

我在OS X El Capitan上不再需要我在下面的原始答案中描述的黑客攻击。如果NSVisualEffectView的{​​{1}}设置为maskImage,那么NSWindow的{​​{1}}应该可以在那里正常工作(如果是contentView则不够NSVisualEffectView)的子视图。

以下是一个示例项目:https://github.com/marcomasser/OverlayTest


原始答案 - 仅与OS X Yosemite相关

我通过覆盖私有的NSWindow方法找到了一种方法:contentView。只需返回通过绘制带有圆角矩形的NSBezierPath创建的图像,以获得类似于OS X的音量窗口的外观。


在我的测试中,我发现你需要为NSVisualEffectView 使用掩码图像和 NSWindow。在您的代码中,您使用视图的图层的- (NSImage *)_cornerMask属性来获取圆角,但您可以通过使用蒙版图像来实现相同的效果。在我的代码中,我生成一个由NSVisualEffectView和NSWindow使用的NSImage:

cornerRadius

然后我创建了一个NSWindow子类,它有一个掩码图像的setter:

func maskImage(#cornerRadius: CGFloat) -> NSImage {
    let edgeLength = 2.0 * cornerRadius + 1.0
    let maskImage = NSImage(size: NSSize(width: edgeLength, height: edgeLength), flipped: false) { rect in
        let bezierPath = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
        NSColor.blackColor().set()
        bezierPath.fill()
        return true
    }
    maskImage.capInsets = NSEdgeInsets(top: cornerRadius, left: cornerRadius, bottom: cornerRadius, right: cornerRadius)
    maskImage.resizingMode = .Stretch
    return maskImage
}

然后,在我的NSWindowController子类中,我为视图和窗口设置了掩码图像:

class MaskedWindow : NSWindow {

    /// Just in case Apple decides to make `_cornerMask` public and remove the underscore prefix,
    /// we name the property `cornerMask`.
    @objc dynamic var cornerMask: NSImage?

    /// This private method is called by AppKit and should return a mask image that is used to 
    /// specify which parts of the window are transparent. This works much better than letting 
    /// the window figure it out by itself using the content view's shape because the latter
    /// method makes rounded corners appear jagged while using `_cornerMask` respects any
    /// anti-aliasing in the mask image.
    @objc dynamic func _cornerMask() -> NSImage? {
        return cornerMask
    }

}

如果您将带有该代码的应用程序提交到App Store,我不知道Apple会做什么。你实际上并没有调用任何私有API,你只是覆盖一个碰巧与AppKit中的私有方法同名的方法。你怎么知道有命名冲突?

此外,这不会优雅地失败,你不必做任何事情。如果Apple在内部改变它的工作方式并且该方法不会被调用,那么你的窗口不会得到漂亮的圆角,但一切仍然有效并且看起来几乎相同。


如果您对我如何发现这种方法感到好奇:

我知道操作系统X音量指示做了我想要做的事情,我希望像疯子一样改变音量会导致显着的CPU使用量,从而将音量指示放在屏幕上。因此,我打开了活动监视器,按CPU使用情况排序,激活过滤器以仅显示“我的进程”并敲击我的音量增大/减小键。

很明显,class OverlayWindowController : NSWindowController { @IBOutlet weak var visualEffectView: NSVisualEffectView! override func windowDidLoad() { super.windowDidLoad() let maskImage = maskImage(cornerRadius: 18.0) visualEffectView.maskImage = maskImage if let window = window as? MaskedWindow { window.cornerMask = maskImage } } } 中的coreaudiodBezelUIServer中的/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer做了些什么。通过查看后者的捆绑资源,很明显它负责绘制卷指示。 (注意:该过程仅在显示某些内容后短时间内运行。)

然后我使用Xcode在启动后立即附加到该进程(Debug> Attach to Process> By Process Identifier(PID)或Name ...,然后输入“BezelUIServer”)并再次更改音量。附加调试器后,视图调试器让我看一下视图层次结构,看看该窗口是名为BSUIRoundWindow的NSWindow子类的实例。

在二进制文件上使用class-dump表明这个类是NSWindow的直接后代,只实现了三种方法,而一种是- (id)_cornerMask,这听起来很有希望。

回到Xcode,我使用了Object Inspector(右侧,第三个选项卡)来获取window对象的地址。使用该指针,我通过在lldb中打印其描述来检查这个_cornerMask实际返回的内容:

(lldb) po [0x108500110 _cornerMask]
<NSImage 0x608000070300 Size={37, 37} Reps=(
    "NSCustomImageRep 0x608000082d50 Size={37, 37} ColorSpace=NSCalibratedRGBColorSpace BPS=0 Pixels=0x0 Alpha=NO"
)>

这表明返回值实际上是一个NSImage,这是我实现_cornerMask所需的信息。

如果您想查看该图像,可以将其写入文件:

(lldb) e (BOOL)[[[0x108500110 _cornerMask] TIFFRepresentation] writeToFile:(id)[@"~/Desktop/maskImage.tiff" stringByExpandingTildeInPath] atomically:YES]

为了深入挖掘,您可以使用Hopper Disassembler反汇编BezelUIServerAppKit并生成伪代码,以了解_cornerMask如何实现并用于获取更清楚地了解内部工作原理。不幸的是,关于这种机制的一切都是私有API。

答案 1 :(得分:3)

我记得在CALayer出现之前很久就做过这种事。您可以使用NSBezierPath制作路径。

我不相信你实际上需要来继承NSWindow。关于窗口的重要一点是使用NSBorderlessWindowMask初始化窗口并应用以下设置:

[window setAlphaValue:0.5]; // whatever your desired opacity is
[window setOpaque:NO];
[window setHasShadow:NO];

然后,将窗口的contentView设置为自定义NSView子类,并覆盖drawRect:方法,类似于:

// "erase" the window background
[[NSColor clearColor] set];
NSRectFill(self.frame);

// make a rounded rect and fill it with whatever color you like
NSBezierPath* clipPath = [NSBezierPath bezierPathWithRoundedRect:self.frame xRadius:14.0 yRadius:14.0];
[[NSColor blackColor] set]; // your bg color
[clipPath fill];

结果(忽略滑块):

enter image description here

修改:如果此方法出于任何原因不受欢迎,您是否可以简单地将CAShapeLayer指定为contentView的{​​{1}},然后转换为高于layerNSBezierPath或仅构建为CGPath并指定图层CGPath的路径?

答案 2 :(得分:2)

您所指的“平滑效果”称为“抗锯齿”。我做了一些谷歌搜索,我想你可能是第一个试图绕过NSVisualEffectView角落的人。你告诉CALayer有一个边界半径,它将围绕角落,但你没有设置任何其他选项。我会试试这个:

layer.shouldRasterize = YES;
layer.edgeAntialiasingMask = kCALayerLeftEdge | kCALayerRightEdge | kCALayerBottomEdge | kCALayerTopEdge;

Anti-alias diagonal edges of CALayer

https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/CALayer_class/index.html#//apple_ref/occ/instp/CALayer/edgeAntialiasingMask

答案 3 :(得分:2)

尽管NSVisualEffectView没有抗锯齿边缘的限制,但现在这里的kludgey解决方法应该适用于没有阴影的浮动无标题的不可重现窗口的应用程序 - 在下面有一个子窗口,只绘制边缘。

我能让我看起来像这样:

enter image description here

执行以下操作:

在控制器中控制一切:

- (void) create {

NSRect windowRect = NSMakeRect(100.0, 100.0, 200.0, 200.0);
NSRect behindWindowRect = NSMakeRect(99.0, 99.0, 202.0, 202.0);
NSRect behindViewRect = NSMakeRect(0.0, 0.0, 202.0, 202.0);

NSRect viewRect = NSMakeRect(0.0, 0.0, 200.0, 200.0);

window = [FloatingWindow createWindow:windowRect];

behindAntialiasWindow = [FloatingWindow createWindow:behindWindowRect];
roundedHollowView = [[RoundedHollowView alloc] initWithFrame:behindViewRect];

[behindAntialiasWindow setContentView:roundedHollowView];
[window addChildWindow:behindAntialiasWindow ordered:NSWindowBelow];

backingView = [[NSView alloc] initWithFrame:viewRect];

contentView = [[NSVisualEffectView alloc] initWithFrame:viewRect];
[contentView setWantsLayer:NO];
[contentView setState:NSVisualEffectStateActive];
[contentView setAppearance:
 [NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]];
[contentView setMaskImage:[AppDelegate maskImageWithBounds:contentView.bounds]];

[backingView addSubview:contentView];

[window setContentView:backingView];
[window setLevel:NSFloatingWindowLevel];
[window orderFront:self];


}

+ (NSImage *) maskImageWithBounds: (NSRect) bounds
{
return [NSImage imageWithSize:bounds.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {

    NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:bounds xRadius:20.0 yRadius:20.0];

    [path setLineJoinStyle:NSRoundLineJoinStyle];
    [path fill];

    return YES;
}];
}

RoundedHollowView的画面如下:

- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// "erase" the window background
[[NSColor clearColor] set];
NSRectFill(self.frame);

[[NSColor colorWithDeviceWhite:1.0 alpha:0.7] set];

NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:20.0 yRadius:20.0];
path.lineWidth = 2.0;
[path stroke];

}

同样,这是一个黑客,您可能需要使用lineWidth / alpha值取决于您使用的基本颜色 - 在我的示例中,如果您看起来非常接近或在较浅的背景下,您将会找到边框一点点,但对于我自己的用途,它感觉不像没有任何抗锯齿那样刺耳。

请记住,混合模式不会像音量控制那样与原生osx yosemite弹出式窗口相同 - 这些弹出窗口似乎使用了一个不同的未记录的后窗外观,它显示了更多的色彩效果。

答案 4 :(得分:2)

所有对Marco Masser的最佳解决方案的赞誉,有两点有用:

  1. 要使平滑的圆角起作用,NSVisualEffectView必须是视图控制器中的根视图。
  2. 当使用深色材质时,仍然会在黑暗的背景上显得有趣的浅色裁剪边缘。使窗口背景透明以避免这种情况,window.backgroundColor = NSColor.clearColor()

    enter image description here

答案 5 :(得分:2)

这些解决方案均不适用于Mojave。但是,经过一个小时的研究,我发现this amazing repo展示了不同的窗口设计。解决方案之一看起来像是OP所需的外观。我尝试了一下,它的抗锯齿圆角效果很好,没有标题栏伪像。这是工作代码:

let visualEffect = NSVisualEffectView()
visualEffect.translatesAutoresizingMaskIntoConstraints = false
visualEffect.material = .dark
visualEffect.state = .active
visualEffect.wantsLayer = true
visualEffect.layer?.cornerRadius = 16.0

window?.titleVisibility = .hidden
window?.styleMask.remove(.titled)
window?.backgroundColor = .clear
window?.isMovableByWindowBackground = true

window?.contentView?.addSubview(visualEffect)

请注意在contentView.addSubview(visualEffect)而不是contentView = visualEffect的末尾。这是使其工作的关键之一。