sierra night mojave mac high for enable developer dark objective-c macos cocoa nswindow nsvisualeffectview

objective-c - night - xcode mac os sierra



¿Cómo hacer una ventana OS X suave, redondeada y similar a un volumen con NSVisualEffectView? (5)

Actualmente estoy intentando hacer una ventana que se parece a la ventana Volume OS X:


Para hacer esto, tengo mi propia NSWindow (usando una subclase personalizada), que es transparent / sin barra de título / shadow-less, que tiene un NSVisualEffectView dentro de su contentView. Aquí está el código de mi subclase para hacer que la vista de contenido sea redonda:

- (void)setContentView:(NSView *)aView { aView.wantsLayer = YES; aView.layer.frame = aView.frame; aView.layer.cornerRadius = 14.0; aView.layer.masksToBounds = YES; [super setContentView:aView]; }


Y aquí está el resultado (como se puede ver, las esquinas son granulosas, OS X son mucho más suaves):

¿Alguna idea sobre cómo hacer las esquinas más suaves? Gracias


Actualización para OS X El Capitan

El truco que describí en mi respuesta original a continuación ya no es necesario en OS X El Capitán. La NSVisualEffectView de maskImage debería funcionar correctamente allí, si el NSWindow de contentView está configurado para ser NSVisualEffectView (no es suficiente si se trata de una subvista de contentView ).

Aquí hay un proyecto de muestra: https://github.com/marcomasser/OverlayTest

Respuesta original: solo relevante para OS X Yosemite

Encontré una manera de hacerlo al reemplazar un método privado NSWindow: - (NSImage *)_cornerMask . Simplemente devuelva una imagen creada dibujando un NSBezierPath con un rectángulo redondeado para obtener una apariencia similar a la ventana de volumen de OS X.

En mis pruebas, descubrí que debe usar una imagen de máscara para NSVisualEffectView y NSWindow. En tu código, estás usando la propiedad cornerRadius la capa de la cornerRadius para obtener las esquinas redondeadas, pero puedes lograr lo mismo usando una imagen de máscara. En mi código, genero un NSImage utilizado tanto por NSVisualEffectView como por 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 }

Luego creé una subclase NSWindow que tiene un setter para la imagen de la máscara:

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

Luego, en mi subclase NSWindowController configuré la imagen de la máscara para la vista y la ventana:

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

No sé qué hará Apple si envía una aplicación con ese código a la tienda de aplicaciones. En realidad, no estás llamando a ninguna API privada, solo anulas un método que tiene el mismo nombre que un método privado en AppKit. ¿Cómo se debe saber que hay un conflicto de nombres? 😉

Además, esto falla con gracia sin que tengas que hacer nada. Si Apple cambia la forma en que esto funciona internamente y no se llamará al método, tu ventana no tendrá las esquinas redondeadas, pero todo sigue funcionando y se ve casi igual.

Si tiene curiosidad acerca de cómo descubrí este método:

Sabía que la indicación de volumen de OS X hizo lo que quería hacer y esperaba que al cambiar el volumen como un loco resultara en un uso notable de la CPU por el proceso que pone esa indicación de volumen en la pantalla. Por lo tanto, abrí el Monitor de actividad, ordenado por uso de CPU, activé el filtro para mostrar solo "Mis procesos" y martillé mis teclas de subir / bajar volumen.

Quedó claro que coreaudiod y algo llamado BezelUIServer en /System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer hicieron algo. Al observar los recursos del paquete para este último, fue evidente que es responsable de dibujar la indicación de volumen. (Nota: ese proceso solo se ejecuta por un corto tiempo después de que muestra algo).

Luego usé Xcode para adjuntarlo a ese proceso tan pronto como se lanzó (Depurar> Adjuntar a proceso> Por identificador de proceso (PID) o Nombre ..., luego ingresar "BezelUIServer") y volver a cambiar el volumen. Después de adjuntar el depurador, el depurador de vista me permitió echar un vistazo a la jerarquía de vistas y ver que la ventana era una instancia de una subclase BSUIRoundWindow llamada BSUIRoundWindow .

El uso class-dump en el binario mostró que esta clase es un descendiente directo de NSWindow y solo implementa tres métodos, mientras que uno es - (id)_cornerMask , que sonaba prometedor.

De vuelta en Xcode, utilicé el Inspector de Objetos (lado derecho, tercera pestaña) para obtener la dirección del objeto de la ventana. Utilizando ese puntero, verifiqué qué devuelve _cornerMask imprimiendo su descripción en lldb:

(lldb) po [0x108500110 _cornerMask] <NSImage 0x608000070300 Size={37, 37} Reps=( "NSCustomImageRep 0x608000082d50 Size={37, 37} ColorSpace=NSCalibratedRGBColorSpace BPS=0 Pixels=0x0 Alpha=NO" )>

Esto muestra que el valor de retorno en realidad es un NSImage, que es la información que necesitaba para implementar _cornerMask .

Si desea ver esa imagen, puede escribirla en un archivo:

(lldb) e (BOOL)[[[0x108500110 _cornerMask] TIFFRepresentation] writeToFile:(id)[@"~/Desktop/maskImage.tiff" stringByExpandingTildeInPath] atomically:YES]

Para profundizar un poco, puede usar Hopper Disassembler para desmontar BezelUIServer y BezelUIServer y generar pseudocódigo para ver cómo se implementa y usa _cornerMask para obtener una imagen más clara de cómo funcionan las _cornerMask internas. Lamentablemente, todo lo relacionado con este mecanismo es API privada.


A pesar de las limitaciones de los bordes NSVisualEffectView no antialiasing, aquí hay una solución alternativa por ahora que debería funcionar para esta aplicación de una ventana flotante sin título flotante sin sombra: tiene una ventana secundaria debajo que dibuja solo los bordes.

Pude hacer que los míos se vieran así:

haciendo lo siguiente:

En un controlador que contiene todo:

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

El drawrect de RoundedHollowView tiene este aspecto:

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

De nuevo, este es un truco y es posible que necesite jugar con los valores lineWidth / alpha dependiendo del color base que use; en mi ejemplo, si mira muy de cerca o con fondos más claros, verá un poco el borde, pero para mi propio uso se siente menos discordante que no tener ningún antialiasing.

Tenga en cuenta que el modo de fusión no será el mismo que el de las ventanas emergentes nativas de osx yosemite, como el control de volumen, que parecen usar una apariencia diferente detrás de la ventana sin documentar que muestra más de un efecto de grabación de color.


El "efecto suave" al que se refiere se llama "Antialiasing". Hice un poco de google y creo que podrías ser la primera persona que ha intentado rodear las esquinas de un NSVisualEffectView. Le dijo al CALayer que tenga un radio de borde, que redondeará las esquinas, pero no configuró ninguna otra opción. Intentaré esto:

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

Bordes diagonales anti-alias de CALayer

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


Recuerdo haber hecho este tipo de cosas mucho antes de que CALayer estuviera cerca. Utiliza NSBezierPath para hacer la ruta.

No creo que realmente necesite subclase NSWindow . El NSBorderlessWindowMask importante de la ventana es inicializar la ventana con NSBorderlessWindowMask y aplicar la siguiente configuración:

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

A continuación, configura el contentView de su ventana en una subclase NSView personalizada con el método drawRect: anulado de manera similar a esto:

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

resultado (ignorar el control deslizante):

Editar : Si este método es indeseable por alguna razón, ¿no puedes simplemente asignar un CAShapeLayer como la layer contentView , luego convertir el NSBezierPath anterior a CGPath o simplemente construir como un CGPath y asignar la ruta a la ruta de las capas?


Todas las felicitaciones a Marco Masser por la solución más clara, hay dos puntos útiles:

  1. Para que las esquinas redondeadas lisas funcionen, NSVisualEffectView debe ser la vista raíz dentro del controlador de visualización.
  2. Al utilizar el material oscuro, todavía hay bordes graciosos y ligeros recortados que se vuelven muy evidentes en el fondo oscuro. Haga que el fondo de su ventana sea transparente para evitar esto, window.backgroundColor = NSColor.clearColor() .