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 では不要になりました。NSVisualEffectViewがに設定されている場合maskImage、 はそこで正しく機能するはずです(のサブビューである場合は十分ではありません)。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 の直接の子孫であり、3 つのメソッドのみを実装していることが示されましたが、そのうちの 1 つは- (id)_cornerMask有望に思えました。

Xcode に戻り、オブジェクト インスペクター (右側、3 番目のタブ) を使用してウィンドウ オブジェクトのアドレスを取得しました。そのポインターを使用して_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 逆アセンブラーを使用して逆アセンブルBezelUIServerAppKit疑似コードを生成して、 がどのように_cornerMask実装され、使用されているかを確認し、内部がどのように機能するかをより明確に理解することができます。残念ながら、このメカニズムに関するすべてがプライベート API です。

于 2015-04-01T09:13:33.403 に答える
4

この種のことをずっと前にやっていたことを覚えています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];

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

ここに画像の説明を入力

編集:この方法が何らかの理由で望ましくない場合は、単純に aCAShapeLayerをあなたcontentViewのものとして割り当ててから、上記をにlayer変換するか、単に a として構築してパスをレイヤーに割り当てることはできませんか?NSBezierPathCGPathCGPathpath

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

}

繰り返しますが、これはハックであり、使用する基本色に応じて lineWidth / alpha 値をいじる必要がある場合があります。私自身の使用では、アンチエイリアスを使用しない場合よりも不快に感じません。

ブレンディング モードは、ボリューム コントロールのようなネイティブの osx ヨセミテ ポップアップと同じではないことに注意してください。

于 2015-01-31T20:15:23.003 に答える
2

Marco Masser が最も優れたソリューションを提供してくれたことに敬意を表します。次の 2 つの便利な点があります。

  1. 滑らかな丸みを帯びた角が機能するには、NSVisualEffectViewView Controller 内のルート ビューである必要があります。
  2. 暗い素材を使用すると、暗い背景で非常に目立つ、面白い光のトリミングされたエッジがまだあります。これを避けるには、ウィンドウの背景を透明にしてくださいwindow.backgroundColor = NSColor.clearColor()

    ここに画像の説明を入力

于 2016-01-24T22:08:53.357 に答える
2

これらのソリューションはどれも、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.addSubview(visualEffect)最後に の代わりに注意してくださいcontentView = visualEffect。これは、それを機能させるための鍵の 1 つです。

于 2019-10-24T02:09:30.647 に答える