Как сделать гладкое, округлое, объемное окно OS X с помощью NSVisualEffectView?

Сейчас я пытаюсь создать окно, похожее на окно 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 более гладкие):
введите здесь описание изображения
Есть идеи, как сделать углы более гладкими? Спасибо


person Pedro Vieira    schedule 22.10.2014    source источник
comment
В iOS скругленные углы, созданные +[UIBezierPath bezierPathWithRoundedRect:cornerRadius:], используют новую, более мягкую кривую скругления, чем скругление углов слоя. Интересно, верно ли то же самое для NSBezierPath в OS X? Вот подробное описание новых возможностей округления, а также код, который вы можете взять и использовать, если обнаружите, что он не является частью NSBezierPath: paintcodeapp.com/news/code-for-ios-7-скругленные-прямоугольники   -  person Zev Eisenberg    schedule 23.10.2014
comment
как насчет layer.allowsEdgeAntialiasing = YES;?   -  person Brad Allred    schedule 06.11.2014
comment
@BradAllred layer.allowsEdgeAntialiasing специфичен для iOS и недоступен в OSX. эквивалент OSX устанавливает layer.edgeAntialiasingMask, хотя, судя по попытке, он не работает на NSVisualEffectView, и установка layer.cornerRadius также не работает.   -  person Vivek Gani    schedule 11.01.2015
comment
FWIW, я пробовал несколько вариантов без везения. @pedro, вы можете подать радар на это ( rdar://19589105 здесь)   -  person Vivek Gani    schedule 24.01.2015


Ответы (6)


Обновление для OS X El Capitan

Взлом, который я описал в своем первоначальном ответе ниже, больше не нужен в OS X El Capitan. maskImage NSVisualEffectView должно там работать корректно, если contentView NSWindow установлено как NSVisualEffectView (недостаточно, если это подвид contentView).

Вот пример проекта: https://github.com/marcomasser/OverlayTest.


Оригинальный ответ — актуально только для OS X Yosemite

Я нашел способ сделать это, переопределив частный метод NSWindow: - (NSImage *)_cornerMask. Просто верните изображение, созданное путем рисования NSBezierPath со скругленным прямоугольником, чтобы получить вид, похожий на окно тома OS X.


При тестировании я обнаружил, что вам нужно использовать изображение маски для NSVisualEffectView и NSWindow. В своем коде вы используете свойство 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
        }
    }
}

Я не знаю, что сделает Apple, если вы отправите приложение с этим кодом в App Store. На самом деле вы не вызываете какой-либо частный API, вы просто переопределяете метод, который имеет то же имя, что и частный метод в AppKit. Как узнать о конфликте имен? ????

Кроме того, это изящно терпит неудачу, и вам не нужно ничего делать. Если Apple изменит то, как это работает внутри, и метод просто не будет вызываться, ваше окно не получит красивых закругленных углов, но все по-прежнему будет работать и выглядеть почти так же.


Если вам интересно, как я узнал об этом методе:

Я знал, что индикация громкости OS X делает то, что я хочу, и я надеялся, что изменение громкости как сумасшедшее приведет к заметному использованию ЦП процессом, который выводит эту индикацию громкости на экран. Поэтому я открыл монитор активности, отсортированный по использованию ЦП, активировал фильтр, чтобы показывать только «Мои процессы», и нажал клавиши увеличения/уменьшения громкости.

Стало ясно, что 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, чтобы разобрать BezelUIServer и AppKit и сгенерировать псевдокод, чтобы увидеть, как _cornerMask реализован и используется, чтобы получить более четкое представление о том, как работает внутреннее устройство. К сожалению, все, что касается этого механизма, является частным API.

person Marco Masser    schedule 01.04.2015
comment
Спасибо за вдумчивые подробности об исследовании. - person diimdeep; 21.04.2015
comment
Потрясающий ответ! Спасибо. Я думаю, что этот подход дает наилучшие графические результаты. Я также думаю, что API-интерфейс -[NSWindow _cornerMask] имеет большой смысл, я надеюсь, что он появится как общедоступный API в будущем выпуске. - person joerick; 28.04.2015
comment
Вот реализация вышеизложенного на Objective-C для перехода в подкласс NSWindow: /d52f188b926835b1f655 - person joerick; 28.04.2015
comment
Есть ли что-то еще, что нужно сделать, кроме того, чтобы сделать contentView замаскированным NSVisualEffectView, чтобы получить правильное поведение на Эль-Капитане? У меня есть NSPanel с фоном NSBorderlessWindowMask и clearColor, но края все равно получаются неровными. - person chockenberry; 17.09.2015
comment
Я только вчера обновил код для Micro Snitch (obdev.at/microsnitch), и он работал нормально. Я также использую NSWindow с NSBorderlessWindowMask, а contentView устанавливается на NSVisualEffectView в Interface Builder. Я также создал пример проекта здесь: github.com/marcomasser/OverlayTest - person Marco Masser; 19.09.2015
comment
Любая идея, как отобразить его поверх других окон (т.е. других приложений)? Я пытаюсь отобразить такой виджет, когда пользователь просматривает полноэкранные окна. - person themihai; 11.02.2018
comment
@themihai: Вам стоит взглянуть на свойства NSWindow.level и NSWindow.collectionBehavior. - person Marco Masser; 13.02.2018
comment
К сожалению, в macOS Mojave эта техника не работает, маска странным образом растягивается. - person King Loui; 04.07.2018
comment
Да, я могу подтвердить, что мой пример кода на GitHub не работает в предварительной версии macOS Mojave для разработчиков. Но у меня нет времени разбираться в этом прямо сейчас ???? - person Marco Masser; 04.07.2018

Я помню, как делал подобные вещи задолго до того, как появился 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];

результат (игнорировать ползунок):

введите здесь описание изображения

Редактировать: если этот метод по какой-то причине нежелателен, не могли бы вы просто назначить CAShapeLayer как layer вашего contentView, а затем либо преобразовать указанное выше NSBezierPath в CGPath, либо просто построить как CGPath и назначить путь к слоям path?

person Brad Allred    schedule 25.10.2014
comment
Всем привет! Спасибо за Ваш ответ. Дело в том, что вы прямо рисуете черный закругленный вид, я хочу, чтобы было окно, а внутри этого окна круглое NSVisualEffectView (точно так же, как окно объема OS X). Есть ли способ заполнить NSBezierPath этим NSVisualEffectView, чтобы сделать его границы закругленными без всего этого зерна? - person Pedro Vieira; 25.10.2014
comment
@PedroVieira Я не понимаю проблемы, NSVisualEffectView - это просто подкласс NSView, не так ли? вы не можете просто разместить его как подвид этого представления? просто представьте, что этот вид является окном (я еще не обновился до Yosemite, поэтому не могу проверить) - person Brad Allred; 25.10.2014
comment
@BradAllred Это не использует NSVisualEffectView. При применении CAShapeLayer вы получаете неровный край, как показано выше в исходном вопросе. - person Luke; 16.02.2015

«Сглаживающий эффект», о котором вы говорите, называется «Сглаживание». Я немного погуглил и думаю, что вы можете быть первым, кто попытался скруглить углы 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

person slf    schedule 30.10.2014
comment
Спасибо за ваш ответ. Я попробовал то, что вы сказали, и мой NSVisualEffect все еще имеет эти зернистые края. Может быть, ему не хватает другого свойства или чего-то еще. Вот код, который я использую (_roundView — это мой NSVisualEffectView): pastebin.com/utrMTicj - person Pedro Vieira; 30.10.2014

Несмотря на ограничения 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;
}];
}

Drawrect RoundedHollowView выглядит так:

- (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, таких как регулятор громкости — они, похоже, используют другой недокументированный внешний вид за окном, который показывает больше эффекта выгорания цвета.

person Vivek Gani    schedule 31.01.2015
comment
выглядит как лучший обходной путь. попробую. Спасибо, что поделился - person Pedro Vieira; 01.02.2015

Спасибо Марко Массеру за самое аккуратное решение, есть два полезных момента:

  1. Чтобы сглаженные закругленные углы работали, NSVisualEffectView должен быть корневым представлением в контроллере представления.
  2. При использовании темного материала все еще остаются забавные светлые обрезанные края, которые становятся очень заметными на темном фоне. Чтобы избежать этого, сделайте фон окна прозрачным, window.backgroundColor = NSColor.clearColor().

    введите здесь описание изображения

person Ian Bytchek    schedule 24.01.2016

Ни одно из этих решений не помогло мне в Мохаве. Однако после часа поисков я нашел этот удивительный репозиторий, в котором демонстрируются различные дизайны окон. Одно из решений выглядит как желаемый вид ОП. Я попробовал, и он работал с хорошо сглаженными закругленными углами и без артефактов в заголовке. Вот рабочий код:

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 в конце. Это один из ключей к тому, чтобы заставить его работать.

person lwouis    schedule 24.10.2019