如何使用NSVisualEffectView制作一个光滑的,圆的,类似体积的OS X窗口?

问题描述 投票:23回答:6

我目前正在尝试制作一个类似于Volume OS X窗口的窗口:“在此处输入图像描述”

为此,我有自己的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更加平滑):“在此处输入图像描述”关于如何使转角更平滑的任何想法?谢谢

objective-c macos cocoa nswindow nsvisualeffectview
6个回答
24
投票

OS X El Capitan的更新

我在下面的原始答案中描述的hack在OS X El Capitan上不再需要。如果将NSVisualEffectViewmaskImage设置为NSWindow,则contentViewNSVisualEffectView应该可以在此处正常工作(如果它只是contentView的子视图,则还不够)。

这里是一个示例项目:https://github.com/marcomasser/OverlayTest


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

我找到了一种方法来通过重写私有NSWindow方法:- (NSImage *)_cornerMask。只需返回通过绘制带有圆角矩形的NSBezierPath创建的图像,即可获得类似于OS X的体积窗口的外观。


在我的测试中,我发现您需要为NSVisualEffectView NSWindow使用蒙版图像。在代码中,您使用的是视图图层的cornerRadius属性来获取圆角,但是可以通过使用蒙版图像来实现相同的目的。在我的代码中,我生成了NSVisualEffectView和NSWindow都使用的NSImage:

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
}

然后我创建了一个NSWindow子类,该子类具有用于蒙版图像的设置器:

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
    }

}

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

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
        }
    }
}

我不知道如果您将带有该代码的应用提交到App Store,Apple会怎么做。您实际上并没有调用任何私有API,只是覆盖了恰好与AppKit中的私有方法同名的方法。您怎么知道存在命名冲突? 😉

此外,您无需执行任何操作即可优雅地失败。如果Apple改变了其内部工作方式,而该方法仅被调用,则您的窗口将无法获得漂亮的圆角,但是一切仍然有效,并且看起来almost相同。


如果您对我对这种方法的了解感到好奇:

我知道OS X音量指示可以完成我想做的事情,我希望像疯子一样更改音量,这会导致在屏幕上显示该音量指示的过程导致CPU占用率显着。因此,我打开了活动监视器,并按CPU使用率进行了排序,激活了过滤器以仅显示“我的进程”,并敲定了我的音量上/下键。

[很明显coreaudiod和在BezelUIServer中称为/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer的东西做了什么。通过查看后者的捆绑包资源,很明显,它负责绘制音量指示。 (注意:该过程在显示内容后仅运行一小段时间。)

然后,我使用Xcode在启动后立即将其附加到该进程(调试>附加到进程>按进程标识符(PID)或名称…,然后输入“ BezelUIServer”),然后再次更改音量。附加调试器后,视图调试器使我了解了视图层次结构,并发现该窗口是名为BSUIRoundWindow的NSWindow子类的实例。

在二进制文件上使用class-dump表明该类是NSWindow的直接后代,仅实现三种方法,而其中一个是class-dump,听起来很有希望。

在Xcode中,我使用对象检查器(右侧,第三个选项卡)获取窗口对象的地址。使用该指针,我通过在lldb中打印其描述来检查此- (id)_cornerMask实际返回的内容:

_cornerMask

这表明返回值实际上是一个NSImage,这是我实现(lldb) po [0x108500110 _cornerMask] <NSImage 0x608000070300 Size={37, 37} Reps=( "NSCustomImageRep 0x608000082d50 Size={37, 37} ColorSpace=NSCalibratedRGBColorSpace BPS=0 Pixels=0x0 Alpha=NO" )> 所需的信息。

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

_cornerMask

要更深入一点,您可以使用(lldb) e (BOOL)[[[0x108500110 _cornerMask] TIFFRepresentation] writeToFile:(id)[@"~/Desktop/maskImage.tiff" stringByExpandingTildeInPath] atomically:YES] 分解Hopper DisassemblerBezelUIServer并生成伪代码以查看AppKit的实现方式,并用于更清晰地了解内部原理。不幸的是,与该机制有关的一切都是私有API。

我记得在_cornerMask出现之前很早就做了这种事情。您使用CALayer创建路径。

我不相信您实际上需要

继承NSBezierPath的子类。关于窗口的重要一点是用NSWindow初始化窗口并应用以下设置:
NSBorderlessWindowMask

然后您将窗口的[window setAlphaValue:0.5]; // whatever your desired opacity is [window setOpaque:NO]; [window setHasShadow:NO]; 设置为自定义contentView子类,并使用NSView方法覆盖,类似于以下内容:

drawRect:

结果(忽略滑块):

<< img src =“ https://image.soinside.com/eyJ1cmwiOiAiaHR0cHM6Ly9pLnN0YWNrLmltZ3VyLmNvbS9yS0Q2Ty5wbmcifQ==” alt =“在此处输入图像描述”>

Edit

:如果由于某种原因不希望使用此方法,是否不能简单地将// "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]; 分配为CAShapeLayercontentView,然后将上述layer转换为NSBezierPath还是仅构造为CGPath并将路径分配给图层CGPath

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

path

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

Anti-alias diagonal edges of CALayer

尽管NSVisualEffectView的局限性不是对边缘进行抗锯齿,但是现在这是一个笨拙的解决方法,适用于此浮动无标题,不可调整大小且没有阴影的窗口的应用程序-在子窗口下方仅画出边缘。

我能够使我看起来像这样:

<< img src =“ https://image.soinside.com/eyJ1cmwiOiAiaHR0cHM6Ly9pLnN0YWNrLmltZ3VyLmNvbS9neTdVei5wbmcifQ==” alt =“在此处输入图像描述”>“ >>

通过执行以下操作:

在拥有所有内容的控制器中:

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

RoundedHollowView的drawrect看起来像这样:

- (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;
}];
}

同样,这是一种技巧,您可能需要根据所使用的基色来使用lineWidth / alpha值-在我的示例中,如果您看上去很近或在较浅的背景下,会稍微划一下边框,但就我自己的使用而言,与没有任何抗锯齿功能相比,它感觉不那么刺耳。

请记住,混合模式将与音量控制之类的osx优胜美地本机弹出窗口不同-那些弹出窗口似乎使用了不同的,未记录的幕后外观,显示出更多的色彩燃烧效果。

对于Marco Masser的最简洁的解决方案,所有的荣誉,有两点有用:

  1. 为了使平滑的圆角起作用,- (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]; } 必须是视图控制器内的根视图。
  • 使用深色材料时,仍有有趣的浅色裁切边缘,这些边缘在深色背景上非常明显。为避免这种情况,请使窗口背景透明,NSVisualEffectView

  • window.backgroundColor = NSColor.clearColor()

    这些解决方案都无法在Mojave上为我工作。但是,经过一个小时的研究,我发现enter image description here展示了不同的窗户设计。解决方案之一看起来像是OP所需的外观。我尝试了一下,它的抗锯齿圆角效果很好,没有标题栏伪像。这是工作代码:

    this amazing repo

    [最后请注意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)。这是使其工作的关键之一。


    3
    投票

    我记得在_cornerMask出现之前很早就做了这种事情。您使用CALayer创建路径。


    2
    投票

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


    2
    投票

    尽管NSVisualEffectView的局限性不是对边缘进行抗锯齿,但是现在这是一个笨拙的解决方法,适用于此浮动无标题,不可调整大小且没有阴影的窗口的应用程序-在子窗口下方仅画出边缘。


    2
    投票

    对于Marco Masser的最简洁的解决方案,所有的荣誉,有两点有用:

    1. 为了使平滑的圆角起作用,- (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]; } 必须是视图控制器内的根视图。

    0
    投票

    这些解决方案都无法在Mojave上为我工作。但是,经过一个小时的研究,我发现enter image description here展示了不同的窗户设计。解决方案之一看起来像是OP所需的外观。我尝试了一下,它的抗锯齿圆角效果很好,没有标题栏伪像。这是工作代码:

    this amazing repo
    © www.soinside.com 2019 - 2024. All rights reserved.