objective-c - 如何使用 NSVisualEffectView 制作平滑、圆润、类似体积的 OS X 窗口?

标签 objective-c macos cocoa nswindow nsvisualeffectview

我目前正在尝试制作一个看起来像 Volume OS X 窗口的窗口:
enter image description here

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

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

关于如何使角落更平滑的任何想法?谢谢

最佳答案

OS X El Capitan 更新

OS X El Capitan 不再需要我在下面的原始答案中描述的 hack。 NSVisualEffectViewmaskImage如果 NSWindow 应该在那里正常工作的 contentView设置为 NSVisualEffectView (如果是 contentView 的 subview 是不够的)。

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

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

我找到了一种通过覆盖私有(private) NSWindow 方法来做到这一点的方法:- (NSImage *)_cornerMask .简单地返回一个通过绘制一个带有圆角矩形的 NSBezierPath 创建的图像,以获得类似于 OS X 的音量窗口的外观。

在我的测试中,我发现您需要为 NSVisualEffectView 和 NSWindow 使用 mask 图像。在您的代码中,您使用的是 View 层的 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 子类,它有一个 mask 图像的 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
    }

}

然后,在我的 NSWindowController 子类中,我为 View 和窗口设置了 mask 图像:
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 会怎么做。您实际上并没有调用任何私有(private) API,您只是覆盖了一个恰好与 AppKit 中的私有(private)方法同名的方法。你怎么知道存在命名冲突? 😉

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

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

我知道 OS X 音量指示做了我想做的事情,我希望像疯子一样改变音量会导致将该音量指示显示在屏幕上的进程明显占用 CPU。因此,我打开了事件监视器,按 CPU 使用率排序,激活过滤器以仅显示“我的进程”并敲击我的音量增大/减小键。

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

然后我使用 Xcode 在它启动后立即附加到该进程(调试 > 附加到进程 > 按进程标识符(PID)或名称...,然后输入“BezelUIServer”)并再次更改音量。附加调试器后, View 调试器让我查看 View 层次结构,看到该窗口是名为 BSUIRoundWindow 的 NSWindow 子类的实例。 .

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

回到 Xcode,我使用对象检查器(右侧,第三个选项卡)来获取窗口对象的地址。使用那个指针我检查了这个 _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"
)>

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

如果要查看该图像,可以将其写入文件:
(lldb) e (BOOL)[[[0x108500110 _cornerMask] TIFFRepresentation] writeToFile:(id)[@"~/Desktop/maskImage.tiff" stringByExpandingTildeInPath] atomically:YES]

要深入挖掘,您可以使用 Hopper Disassembler拆机BezelUIServerAppKit并生成伪代码,看看_cornerMask如何实现并用于更清晰地了解内部结构的工作方式。不幸的是,与此机制相关的一切都是私有(private) API。

关于objective-c - 如何使用 NSVisualEffectView 制作平滑、圆润、类似体积的 OS X 窗口?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26518520/

相关文章:

macos - 在 OS X 上设置环境变量

mysql - 通过homebrew安装mysql有什么问题

macos - 对于使用 Cocoa 的 Mac GUI 应用程序有什么好的教程吗?

ios - 我可以为 iPhone 和 iPad 提供两个单独的应用程序吗?

iphone - IO6 不调用 -(BOOL)shouldAutorotate

ios - 使用 NSUserDefaults 的整数值

iOS - 由于未捕获的异常 'NSInvalidArgumentException' 而终止应用程序

cocoa - 在程序中使用 mac 相机的示例代码?

objective-c - NSMutableDictionary 和新项目插入 KVO

objective-c - 在内容上覆盖 NSScroller