Пользовательская кнопка NSButton отображается не так, как ожидалось

#swift #macos #cocoa #appkit #nsbutton

#swift #macos #cocoa #appkit #nsbutton

Вопрос:

Я пытаюсь создать пользовательскую кнопку NSButton с закругленным углом, тенью и плоской.

Вот как я хочу, чтобы это выглядело (демонстрация HTML / CSS здесь): введите описание изображения здесь

(Частичное выделение жирным шрифтом и эффект наведения курсора выходят за рамки)

Я использую этот код:

 @IBDesignable class FlatButton: NSButton {
    @IBInspectable let backgroundColor: NSColor = .white

    override func draw(_ dirtyRect: NSRect) {
        // Set corner radius
        self.wantsLayer = true
        self.layer?.cornerRadius = 18
        self.layer?.borderWidth = 0
        self.layer?.borderColor = backgroundColor.cgColor
        layer?.backgroundColor = backgroundColor.cgColor
        frame.size.height = 32
        
        // Darken background color when highlighted
        if isHighlighted {
            layer?.backgroundColor =  backgroundColor.blended(
                withFraction: 0.2, of: .black
            )?.cgColor
        } else {
            layer?.backgroundColor = backgroundColor.cgColor
        }
        
        self.shadow = NSShadow()
        self.layer?.shadowOffset = CGSize(width: 8, height: 18)
        self.layer?.shadowColor = .black
        self.layer?.shadowRadius = 9
        self.layer?.masksToBounds = true
        self.layer?.shadowOpacity = 0.5
        
        // Super
        super.draw(dirtyRect)
    }
}
  

Но вот что я получаю с этим кодом:

(С границами)

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

(Без границы)

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

Ответ №1:

Возможно, я чего-то не понимаю в вашем вопросе, но создание пользовательской кнопки из подкласса NSView и использование событий мыши, похоже, работают нормально. Функция наведения может быть добавлена с областью отслеживания.

 import Cocoa

var isSelected : Bool
isSelected = false

class CustomView: NSView {

 override func draw(_ rect: NSRect ) {
 super.draw(rect)
 let backgroundColor: NSColor = .white

 // Set corner radius
 self.wantsLayer = true
 self.layer?.cornerRadius = 18
 self.layer?.borderWidth = 0
 self.layer?.borderColor = backgroundColor.cgColor
 layer?.backgroundColor = backgroundColor.cgColor
 frame.size.height = 32
 
 // Shadow
 self.shadow = NSShadow()
 self.layer?.shadowOffset = CGSize(width: 8, height: 18)
 self.layer?.shadowColor = .black
 self.layer?.shadowRadius = 9
 self.layer?.masksToBounds = false
 self.layer?.shadowOpacity = 0.5
       
 // Darken background color when selected
  if isSelected {
   layer?.backgroundColor = backgroundColor.blended( withFraction: 0.2, of: .black )?.cgColor
   } else {
   layer?.backgroundColor = backgroundColor.cgColor
  }

 // Add attributed text
 let text: NSString = "Custom button test"
 let font = NSFont.systemFont(ofSize: 22)
 let attr: [NSAttributedString.Key: Any] = [.font: font,.foregroundColor: NSColor.red]
 text.draw(at:NSMakePoint(50,5), withAttributes:attr)
}

override func mouseDown(with event: NSEvent) {
 print("mouse down in button.")
 isSelected = true
 self.needsDisplay = true 
}

override func mouseUp(with event: NSEvent) {
 print("mouse up in button.")
 isSelected = false
 self.needsDisplay = true
}

}

class ApplicationDelegate: NSObject, NSApplicationDelegate {

func buildMenu() {
let mainMenu = NSMenu()
 NSApp.mainMenu = mainMenu
 // **** App menu **** //
 let appMenuItem = NSMenuItem()
 mainMenu.addItem(appMenuItem)
 let appMenu = NSMenu()
 appMenuItem.submenu = appMenu
 appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
}

func buildWnd() {
 let _wndW : CGFloat = 500
 let _wndH : CGFloat = 400

let window = NSWindow(contentRect: NSMakeRect( 0, 0, _wndW, _wndH ), styleMask:[.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
 window.center()
 window.title = "Swift Test Window"
 window.makeKeyAndOrderFront(window)

// **** Custom view **** //
 let view = CustomView( frame:NSMakeRect(60, _wndH - 160, 300, 32)) 
 view.autoresizingMask = [.width, .height]      
 window.contentView!.addSubview (view)
    
// **** Quit btn **** //
 let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
 quitBtn.bezelStyle = .circular
 quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 quitBtn.title = "Q"
 quitBtn.action = #selector(NSApplication.terminate)
 window.contentView!.addSubview(quitBtn)
}
 
func applicationDidFinishLaunching(_ notification: Notification) {
 buildMenu()
 buildWnd()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
 return true
}

}
let applicationDelegate = ApplicationDelegate()

let application = NSApplication.shared
application.setActivationPolicy(NSApplication.ActivationPolicy.regular)
application.delegate = applicationDelegate
application.activate(ignoringOtherApps:true)
application.run()


  

Комментарии:

1. Спасибо за вашу помощь. Но как мне добавить тень и текст?

2. Мы должны иметь возможность использовать атрибутивную строку для текста и тени. Мне не удалось заставить тень работать в вашем сообщении, поэтому я не уверен, как это должно выглядеть. Я попытаюсь добавить его, если вы покажете мне нужный эффект (возможно, я его пропустил). Пример текста показан в моем отредактированном ответе.

3. Если вы установите для .maskToBounds значение false, тень будет отображаться. Смотрите вторую правку для ответа.

4. Спасибо, я раньше не работал с NSViews подобным образом. Как мне связать действие щелчка из раздела дизайна с viewcontroller? У меня есть несколько кнопок, которые имеют разные действия нажатия

5. Это просто позволяет мне добавить новый выход в viewcontroller. В NSView нет раздела «отправленные действия»

Ответ №2:

Чтобы у вашей функции рисования было меньше работы, при инициализации окна можно было бы выполнить большую часть настройки вида:

 import Cocoa

var isSelected : Bool
isSelected = false

class CustomView: NSView {

 override func draw(_ rect: NSRect ) {
 super.draw(rect)

 let backgroundColor: NSColor = .white
       
 // Darken background color when selected
 if isSelected {
  layer?.backgroundColor = backgroundColor.blended( withFraction: 0.2, of: .black )?.cgColor
  } else {
  layer?.backgroundColor = backgroundColor.cgColor
 }

 // Add attributed text
 let text: NSString = "Custom button text"
 let font = NSFont.systemFont(ofSize: 22)
 let attr: [NSAttributedString.Key: Any] = [.font: font,.foregroundColor: NSColor.red]
 text.draw(at:NSMakePoint(50,5), withAttributes:attr)
}

func myBtnAction( ) {
  NSSound.beep()
}

override func mouseDown(with event: NSEvent) {
 print("mouse down in button.")
 isSelected = true
 self.needsDisplay = true 
}

override func mouseUp(with event: NSEvent) {
 print("mouse up in button.")
 isSelected = false
 myBtnAction()
 self.needsDisplay = true
}

}

class ApplicationDelegate: NSObject, NSApplicationDelegate {

func buildMenu() {
let mainMenu = NSMenu()
 NSApp.mainMenu = mainMenu
 // **** App menu **** //
 let appMenuItem = NSMenuItem()
 mainMenu.addItem(appMenuItem)
 let appMenu = NSMenu()
 appMenuItem.submenu = appMenu
 appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
}

func buildWnd() {
 let _wndW : CGFloat = 500
 let _wndH : CGFloat = 400

let window = NSWindow(contentRect: NSMakeRect( 0, 0, _wndW, _wndH ), styleMask:[.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
 window.center()
 window.title = "Swift Test Window"
 window.makeKeyAndOrderFront(window)

// **** Custom view **** //
 let view = CustomView( frame:NSMakeRect(60, _wndH - 160, 300, 32)) 
 view.autoresizingMask = [.maxXMargin,.minYMargin]       
 view.wantsLayer = true
 view.layer?.borderColor = NSColor.blue.cgColor
 view.layer?.cornerRadius = 18
 view.layer?.borderWidth = 2
 view.layer?.shadowColor = NSColor.black.cgColor
 view.layer?.shadowRadius = 9
 view.layer?.shadowOpacity = 0.5
 view.layer?.masksToBounds = false
 window.contentView!.addSubview (view)

// **** Quit btn **** //
 let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
 quitBtn.bezelStyle = .circular
 quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 quitBtn.title = "Q"
 quitBtn.action = #selector(NSApplication.terminate)
 window.contentView!.addSubview(quitBtn)
}
 
func applicationDidFinishLaunching(_ notification: Notification) {
 buildMenu()
 buildWnd()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
 return true
}

}
let applicationDelegate = ApplicationDelegate()

let application = NSApplication.shared
application.setActivationPolicy(.regular)
application.delegate = applicationDelegate
application.activate(ignoringOtherApps:true)
application.run()


  

Комментарии:

1. Чтобы подключить действие для пользовательской кнопки просмотра, смотрите отредактированный второй ответ. Добавьте новую функцию с именем ‘myBtnAction’ в класс view и вызывайте ее, когда отпущена мышь.

2. Что, если у меня есть несколько NSViews? Как я могу убедиться, что у каждого NSView есть свое собственное действие? Не было бы лучше добавить эти стили в NSButton?

3. Вероятно, пришлось бы использовать отдельный класс для каждой кнопки или, альтернативно, массив представлений в зависимости от того, сколько кнопок у вас есть. Вы также могли бы добавить изображения или приписываемый текст к кнопкам NSButtons, но я не думаю, что вы сможете получить такие красивые закругленные углы, как у пользовательской кнопки; ваш звонок.

4. Другая возможность: bluelemonbits.com/2020/04/04/create-a-nsbutton-in-code-swift

Ответ №3:

Ниже приведена модификация метода создания NSButton в коде, описанном Марком Марсетом (ссылка выше), и она должна соответствовать вашим требованиям. Функция ‘customNSButton’ позволит создавать несколько кнопок с индивидуальными действиями, установленными Target-Action для каждой кнопки. Для «подмигивания» цвета фона используется таймер, а под кнопкой создается тень. Заголовок является атрибутивным текстом.

 import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
var button:NSButton!

@objc func resetBkgrndColor(_ sender:AnyObject) {
 let backgroundColor: NSColor = .white
 button.layer?.backgroundColor = backgroundColor.cgColor
}

@objc func myBtnAction(_ sender:AnyObject ) {
 let backgroundColor: NSColor = .white
 button.layer?.backgroundColor = backgroundColor.blended( withFraction: 0.2, of: .black )?.cgColor
 Timer.scheduledTimer(timeInterval:0.25, target:self, selector:#selector(self.resetBkgrndColor(_:)), userInfo:nil, repeats:false)
 NSSound.beep()
}

func customNSButton( bkgrndColor: NSColor?, borderColor: NSColor?, borderWidth: CGFloat?, cornerRadius: CGFloat? ) -> NSButton {        
 button = NSButton()
    
 button.wantsLayer = true
 button.layer?.backgroundColor = bkgrndColor?.cgColor ?? .clear
 button.layer?.masksToBounds = true
 button.layer!.cornerRadius = cornerRadius ?? 0
 button.layer!.borderColor = borderColor?.cgColor
 button.layer!.borderWidth = borderWidth ?? 0

 button.layer?.shadowOffset = CGSize(width: 8, height: 18)
 button.layer?.shadowColor = .black
 button.layer?.shadowRadius = 9
 button.layer?.masksToBounds = false
 button.layer?.shadowOpacity = 0.5
      
 return button
}

func buildMenu() {
 let mainMenu = NSMenu()
 NSApp.mainMenu = mainMenu
 // **** App menu **** //
 let appMenuItem = NSMenuItem()
 mainMenu.addItem(appMenuItem)
 let appMenu = NSMenu()
 appMenuItem.submenu = appMenu
 appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
}
    
func buildWnd() {
    
let _wndW : CGFloat = 400
let _wndH : CGFloat = 300

 window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
 window.center()
 window.title = "Swift Test Window"
 window.makeKeyAndOrderFront(window)

// **** Custom Button **** //
 let myBtn = customNSButton(bkgrndColor:.white, borderColor:.black, borderWidth:2, cornerRadius:22)
 myBtn.frame = NSMakeRect(60,120,300,40)
 myBtn.bezelStyle = .roundRect
 myBtn.isBordered = false
 myBtn.attributedTitle = NSAttributedString(string: "Btn Text", attributes: [.foregroundColor: NSColor.red, .font:NSFont(name:"Menlo Bold", size:28)])
 myBtn.action = #selector(self.myBtnAction(_:))
 window.contentView!.addSubview(myBtn)

// **** Quit btn **** //
 let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
 quitBtn.bezelStyle = .circular
 quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 quitBtn.title = "Q"
 quitBtn.action = #selector(NSApplication.terminate)
 window.contentView!.addSubview(quitBtn)

}
 
func applicationDidFinishLaunching(_ notification: Notification) {
 buildMenu()
 buildWnd()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
 return true
}

}
let appDelegate = AppDelegate()

// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()