OS X El Capitan 更新
我在下面的原始答案中描述的 hack 在 OS X El Capitan 上不再需要了。这NSVisualEffectView
’s maskImage
应该在那里正常工作,如果NSWindow
’s contentView
被设置为NSVisualEffectView
(如果它是子视图,这是不够的contentView
).
这是一个示例项目:https://github.com/marcomasser/OverlayTest https://github.com/marcomasser/OverlayTest
原始答案 – 仅与 OS X Yosemite 相关
我找到了一种通过重写私有 NSWindow 方法来做到这一点的方法:- (NSImage *)_cornerMask
。只需返回通过绘制带有圆角矩形的 NSBezierPath 创建的图像即可获得类似于 OS X 的音量窗口的外观。
在我的测试中,我发现您需要为 NSVisualEffectView 使用遮罩图像andNSWindow。在您的代码中,您正在使用视图的层cornerRadius
属性来获取圆角,但您可以通过使用蒙版图像来实现相同的目的。在我的代码中,我生成一个 NSImage,供 NSVisualEffectView 和 NSWindow 使用:
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 中的私有方法同名的方法。您如何知道存在命名冲突? ????
此外,这会优雅地失败,而您无需执行任何操作。如果苹果改变了它内部的工作方式,并且该方法不会被调用,你的窗口就不会得到漂亮的圆角,但一切仍然可以工作并且看起来almost相同。
如果你好奇我是如何发现这个方法的:
我知道 OS X 音量指示做了我想做的事情,我希望像疯子一样改变音量会导致将音量指示显示在屏幕上的进程显着占用 CPU。因此,我打开了活动监视器,按 CPU 使用率排序,激活过滤器以仅显示“我的进程”,并敲击音量增大/减小键。
很明显,coreaudiod
和一个叫做BezelUIServer
in /System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer
做了某事。从后者的捆绑资源来看,很明显它负责绘制音量指示。 (注意:该进程仅在显示某些内容后运行一小段时间。)
然后,我使用 Xcode 在该进程启动后立即附加到该进程(“调试”>“附加到进程”>“按进程标识符 (PID) 或名称...”,然后输入“BezelUIServer”)并再次更改音量。连接调试器后,视图调试器让我查看视图层次结构,并看到该窗口是名为 NSWindow 子类的实例BSUIRoundWindow
.
Using class-dump https://github.com/nygard/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]
要更深入地挖掘,您可以使用料斗拆装机 http://www.hopperapp.com拆解BezelUIServer
and AppKit
并生成伪代码来看看如何_cornerMask
的实现和使用是为了更清楚地了解内部结构是如何工作的。不幸的是,与此机制有关的所有内容都是私有 API。