26

我目前正在尝试制作一个看起来像 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];
}


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

4

6 回答 6

24

OS X El Capitan 更新

OS X El Capitan 不再需要我在下面的原始答案中描述的 hack。如果's设置为 the ,则NSVisualEffectView'smaskImage应该在那里正常工作(如果它是 的子视图是不够的)。NSWindowcontentViewNSVisualEffectViewcontentView

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


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

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


在我的测试中,我发现您需要为 NSVisualEffectViewNSWindow 使用蒙版图像。在您的代码中,您使用视图的图层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 在内部更改了它的工作方式并且该方法不会被调用,那么您的窗口不会得到漂亮的圆角,但一切仍然有效并且看起来几乎相同。


如果你好奇我是如何发现这种方法的:

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

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

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

在二进制文件上使用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是如何实现的,并用于更清楚地了解内部是如何工作的。不幸的是,与此机制有关的一切都是私有 API。

于 2015-04-01T09:13:33.403 回答
4

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

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

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

然后,您将contentView窗口NSViewdrawRect:

// "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];

结果(忽略滑块):

在此处输入图像描述

编辑:如果出于某种原因不希望使用此方法,您是否可以不简单地将 a 分配CAShapeLayer为您contentView的,layer然后将上述转换NSBezierPathCGPath或仅构造为 aCGPath并将路径分配给图层path

于 2014-10-25T04:21:35.370 回答
3

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

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

CALayer 的抗锯齿对角线边缘

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

于 2014-10-30T14:51:32.873 回答
2

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

我能够让我的看起来像这样:

在此处输入图像描述

通过执行以下操作:

在一个拥有一切的控制器中:

- (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 的 drawrect 看起来像这样:

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

}

同样,这是一个 hack,您可能需要根据您使用的基色使用 lineWidth / alpha 值 - 在我的示例中,如果您仔细观察或在较浅的背景下看,您会稍微辨认出边框,但是对于我自己的使用感觉不像没有任何抗锯齿那么刺耳。

请记住,混合模式与音量控制等原生 osx yosemite 弹出窗口不同——它们似乎使用不同的未记录的后窗外观,显示更多的颜色燃烧效果。

于 2015-01-31T20:15:23.003 回答
2

感谢 Marco Masser 提供最简洁的解决方案,有两个有用的点:

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

    在此处输入图像描述

于 2016-01-24T22:08:53.357 回答
2

在 Mojave 上,这些解决方案都不适合我。然而,经过一个小时的研究,我发现了这个展示不同窗户设计的惊人回​​购。其中一种解决方案看起来像 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。这是使其发挥作用的关键之一。

于 2019-10-24T02:09:30.647 回答