web-dev-qa-db-ja.com

NSVisualEffectViewを使用して、滑らかで丸みを帯びたボリュームのようなOS Xウィンドウを作成するにはどうすればよいですか?

私は現在、Volume OSXウィンドウのようなウィンドウを作成しようとしています。
enter image description here


これを行うために、私は自分の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ははるかにスムーズです):
enter image description here
コーナーをスムーズにする方法について何かアイデアはありますか?ありがとう

23
Pedro Vieira

OS X ElCapitanのアップデート

以下の元の回答で説明したハックは、OS X ElCapitanではもう必要ありません。 NSVisualEffectViewmaskImageNSWindowに設定されている場合、contentViewNSVisualEffectViewはそこで正しく機能するはずです(そうである場合は十分ではありません) contentViewのサブビュー)。

サンプルプロジェクトは次のとおりです: https://github.com/marcomasser/OverlayTest


元の回答– OS XYosemiteにのみ関連

プライベートNSWindowメソッド- (NSImage *)_cornerMaskをオーバーライドすることでこれを行う方法を見つけました。角丸長方形を含むNSBezierPathを描画して作成した画像を返すだけで、OSXのボリュームウィンドウに似た外観になります。


私のテストでは、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
        }
    }
}
_

そのコードを含むアプリをAppStoreに送信した場合、Appleはどうなるかわかりません。実際には、プライベートAPIを呼び出しているのではなく、発生するメソッドをオーバーライドしているだけです。 AppKitのプライベートメソッドと同じ名前にする必要があります。名前の競合があることをどのように知る必要がありますか?????

その上、これはあなたが何もしなくても優雅に失敗します。 Appleがこれが内部的に機能する方法を変更し、メソッドが呼び出されない場合、ウィンドウは素敵な丸みを帯びた角を取得しませんが、すべてが機能し、見えますほぼ同じ。


この方法について私がどのようにして見つけたかについて知りたい場合は、次のようにします。

OS Xの音量表示が自分のやりたいことをしていることはわかっていたので、狂人のように音量を変更すると、その音量表示を画面に表示するプロセスによってCPU使用率が目立つようになることを期待していました。そのため、CPU使用率でソートされたアクティビティモニターを開き、フィルターをアクティブにして「マイプロセス」のみを表示し、音量を上げたり下げたりしました。

coreaudiodと_/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer_のBezelUIServerと呼ばれるものが何かをしたことが明らかになりました。後者のバンドルリソースを見ると、ボリューム表示を描画する責任があることが明らかでした。 (注:このプロセスは、何かが表示された後、短時間しか実行されません。)

次に、Xcodeを使用して、プロセスが起動したらすぐにそのプロセスに接続し([デバッグ]> [プロセスに接続]> [プロセス識別子(PID)または名前で…]、「BezelUIServer」と入力)、ボリュームを再度変更しました。デバッガーが接続された後、ビューデバッガーを使用して、ビュー階層を確認し、ウィンドウがBSUIRoundWindowというNSWindowサブクラスのインスタンスであることがわかりました。

バイナリで _class-dump_ を使用すると、このクラスはNSWindowの直接の子孫であり、3つのメソッドのみを実装するのに対し、1つは- (id)_cornerMaskであり、有望に聞こえます。

Xcodeに戻り、オブジェクトインスペクター(右側、3番目のタブ)を使用して、ウィンドウオブジェクトのアドレスを取得しました。そのポインターを使用して、lldbに説明を出力することにより、この__cornerMask_が実際に何を返すかを確認しました。

_(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です。

24
Marco Masser

CALayerが登場するずっと前に、この種のことをしたことを覚えています。パスを作成するには、NSBezierPathを使用します。

NSWindowをサブクラス化する必要があるとは思わない。ウィンドウに関する重要な点は、ウィンドウをNSBorderlessWindowMaskで初期化し、次の設定を適用することです。

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

次に、ウィンドウの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];

結果(スライダーは無視してください):

enter image description here

編集:このメソッドが何らかの理由で望ましくない場合は、単にCAShapeLayercontentViewlayerとして割り当ててから、上記のNSBezierPathCGPathに変換するか、単にCGPathとして構築してレイヤーにパスを割り当てることはできません。 path

3
Brad Allred

これらのソリューションはどれも、Mojaveではうまくいきませんでした。しかし、1時間の調査の結果、さまざまなウィンドウデザインを紹介する この驚くべきレポ が見つかりました。解決策の1つは、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 = visualEffect_ではなくcontentView.addSubview(visualEffect)に注意してください。これは、それを機能させるための鍵の1つです。

2
lwouis

NSVisualEffectViewはエッジをアンチエイリアスしないという制限がありますが、これは、影のないフローティングタイトルのないサイズ変更不可能なウィンドウのこのアプリケーションで機能するはずの厄介な回避策です-下にエッジだけを引き出す子ウィンドウがあります。

私は私のように見えるようにすることができました:

enter image description here

次のようにします。

すべてを保持するコントローラーの場合:

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

}

繰り返しますが、これはハックであり、使用するベースカラーに応じてlineWidth/alpha値を試す必要がある場合があります-私の例では、非常に近くまたは明るい背景の下で見ると、境界線が少しわかりますが、私自身の使用では、アンチエイリアスを使用しない場合よりも不快感が少なくなります。

ブレンディングモードは、ボリュームコントロールのようなネイティブのosx yosemiteポップアップと同じではないことに注意してください。これらのポップアップは、より多くのカラーバーン効果を示す、文書化されていない別のウィンドウの背後の外観を使用しているように見えます。

2
Vivek Gani

あなたが言及している「滑らかな効果」は「アンチエイリアス」と呼ばれます。私は少しグーグルをしました、そしてあなたは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

2
slf

最もきちんとした解決策についてのMarcoMasserへのすべての称賛には、2つの有用なポイントがあります。

  1. 滑らかな丸みを帯びた角が機能するには、NSVisualEffectViewがViewController内のルートビューである必要があります。
  2. 暗い素材を使用する場合でも、暗い背景で非常にはっきりと見える面白い明るいトリミングされたエッジがあります。これを回避するには、ウィンドウの背景を透明にします、window.backgroundColor = NSColor.clearColor()

    enter image description here

2
Ian Bytchek