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

Ped*_*ira 23 macos cocoa objective-c nswindow nsvisualeffectview

我目前正在尝试创建一个类似于Volume OS X窗口的窗口:
在此输入图像描述


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

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

   [super setContentView:aView];
}
Run Code Online (Sandbox Code Playgroud)


这就是结果(正如你所看到的,角落是颗粒状的,OS X更平滑):
在此输入图像描述
关于如何使角落更平滑的任何想法?谢谢

Mar*_*ser 24

OS X El Capitan更新

OS X El Capitan不再需要我在下面的原始答案中描述的黑客攻击.该NSVisualEffectViewmaskImage应该正常工作在那里,如果NSWindowcontentView设置是NSVisualEffectView(这是不够的,如果它的一个子视图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
}
Run Code Online (Sandbox Code Playgroud)

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

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
    }

}
Run Code Online (Sandbox Code Playgroud)

然后,在我的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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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


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

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

很明显,coreaudiod所谓BezelUIServer的内容/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer做了些什么.通过查看后者的捆绑资源,很明显它负责绘制卷指示.(注意:该过程仅在显示某些内容后短时间内运行.)

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

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

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

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

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

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

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

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


Bra*_*red 4

我记得很久以前就做过这种事CALayer。您用来NSBezierPath制作路径。

我不相信你真的需要子类化NSWindow。关于窗口的重要一点是使用以下设置初始化窗口NSBorderlessWindowMask并应用以下设置:

[window setAlphaValue:0.5]; // whatever your desired opacity is
[window setOpaque:NO];
[window setHasShadow:NO];
Run Code Online (Sandbox Code Playgroud)

然后,您将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];
Run Code Online (Sandbox Code Playgroud)

结果(忽略滑块):

在此输入图像描述

编辑:如果此方法出于某种原因不受欢迎,您是否可以简单地将 a 指定CAShapeLayer为您的,然后将上面的contentView内容转换为或仅构造为 a并将路径指定给图层?layerNSBezierPathCGPathCGPathpath

  • 你好呀!感谢您的回答。问题是:你直接绘制一个黑色圆形视图,我想要的是有一个窗口,并且在该窗口内有一个圆形的“NSVisualEffectView”(就像 OS X 的卷窗口一样)。有没有办法用 NSVisualEffectView 来“填充”“NSBezierPath”,以便使其边框圆滑而没有所有颗粒? (2认同)