问题
I implement a NSMenu with NSMenuItem and set custom view to it. When menu is scrollable, mouse hovering on ▼ button to scroll will cause some menuItem disappear (or not draw correctly). Hope someone give me some help. I will appreciate that.
Here's video about this issue: https://streamable.com/obrbon
Here's my code:
private func setupMenuItemView(_ menu: NSMenu) {
let menuItemHeight: CGFloat = 20
let menuWidth = frame.width
let textFieldPadding: CGFloat = 10
for menuItem in menu.items {
guard !menuItem.title.isEmpty else { continue }
let menuItemView = MenuItemView(frame: NSRect(x: 0, y: 0, width: Int(menuWidth), height: menuItemHeight))
let textField = MenuItemTextField(labelWithString: menuItem.title)
textField.frame = NSRect(
x: textFieldPadding,
y: (menuItemView.frame.height-textField.frame.height)/2,
width: menuWidth-textFieldPadding*2,
height: textField.frame.height
)
textField.lineBreakMode = .byTruncatingTail
menuItemView.addSubview(textField)
menuItemView.toolTip = menuItem.title
menuItem.view = menuItemView
menuItem.target = self
menuItem.action = #selector(onMenuItemClicked(_:))
}
}
fileprivate class MenuItemView: NSView {
override func mouseUp(with event: NSEvent) {
guard let menuItem = enclosingMenuItem else { return }
guard let action = menuItem.action else { return }
NSApp.sendAction(action, to: menuItem.target, from: menuItem)
menuItem.menu?.cancelTracking()
}
override func draw(_ dirtyRect: NSRect) {
guard let menuItem = enclosingMenuItem else { return }
if menuItem.isHighlighted {
NSColor.alternateSelectedControlColor.set()
} else {
NSColor.clear.set()
}
NSBezierPath.fill(dirtyRect)
super.draw(dirtyRect)
}
}
fileprivate class MenuItemTextField: NSTextField {
override var allowsVibrancy: Bool {
return false
}
}
After calling setupMenuItemView()
, i call menu.popup()
.
Hope this information helps.
回答1:
I was unable to get your posted code to work correctly. The demo below is an alternative which uses a popUpContextual menu with subclassed text fields embedded in the menuItem views (note that the view associated with each menuItem is used and a custom view class is not created). Text alignment and truncation is functional. Menu width is also flexible and may be set to match width of the menu title field. The demo may be run in Xcode by copy/pasting source code into a newly added ‘main.swift’ file and additionally deleting Apple’s AppDelegate class.
import Cocoa
var view = [NSTextField]()
class TextField: NSTextField {
override func mouseDown(with event: NSEvent) {
print("selected = \(self.tag)")
for i:Int in 0..<view.count {
if(self.tag == i) {
view[i].backgroundColor = .lightGray
} else {
view[i].backgroundColor = .clear
}
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
var menu = NSMenu()
let _menuLeft : CGFloat = 40
let _menuTop : CGFloat = 70
let _menuWidth : CGFloat = 70
let _menuItemH : CGFloat = 20
@objc func menuBtnAction(_ sender:AnyObject ) {
let menuOrigin = NSMakePoint(_menuLeft, sender.frame.origin.y - 5)
let wNum : Int = sender.window.windowNumber
let event = NSEvent.mouseEvent(with:.leftMouseDown, location:menuOrigin, modifierFlags:[], timestamp:0, windowNumber:wNum, context:nil, eventNumber:0, clickCount:1, pressure:1.0)
NSMenu.popUpContextMenu(menu, with: event!, for: window.contentView!)
}
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)
let menuItems = ["10%","25%","50%","75%","100%","Longer text."]
menu.title = "Fit"
var count:Int = 0
for mItem in menuItems{
let menuItem = NSMenuItem()
menu.addItem(menuItem)
let textField = TextField(frame:NSMakeRect(0,0,_menuWidth, _menuItemH))
menuItem.view = textField
textField.alignment = .left // .left, .center, .right
textField.stringValue = mItem
textField.lineBreakMode = .byTruncatingTail
textField.backgroundColor = .clear
textField.isEditable = false
textField.tag = count
textField.isBordered = false
textField.font = NSFont( name:"Menlo bold", size:14 )
count = count + 1
view.append(textField)
}
// **** Menu title **** //
let label = NSTextField (frame:NSMakeRect( _menuLeft, _wndH - 50, _menuWidth, 24 ))
window.contentView!.addSubview (label)
label.autoresizingMask = [.maxXMargin,.minYMargin]
label.backgroundColor = .clear
label.lineBreakMode = .byTruncatingTail
label.isSelectable = false
label.isBordered = true
label.font = NSFont( name:"Menlo bold", size:14 )
label.stringValue = menu.title
// **** Menu Disclosure Button **** //
let menuBtn = NSButton (frame:NSMakeRect( (_menuLeft + _menuWidth) - 20, _wndH - 50, 20, 24 ))
menuBtn.bezelStyle = .disclosure
menuBtn.autoresizingMask = [.maxXMargin,.minYMargin]
menuBtn.title = ""
menuBtn.action = #selector(self.menuBtnAction(_:))
window.contentView!.addSubview (menuBtn)
// **** 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()
回答2:
Second possible alternative utilizes an NSPopUpButton with drop down menu which accommodates DarkMode by changing text color. As before, a subclassed text field is embedded into each menuItem.view to support text truncation and alignment. May be run in Xcode with instructions given previously.
import Cocoa
var view = [NSTextField]()
class TextField: NSTextField {
override func mouseDown(with event: NSEvent) {
print("selected = \(self.tag)")
for i:Int in 0..<view.count {
if(self.tag == i) {
view[i].backgroundColor = .lightGray
} else {
view[i].backgroundColor = .windowBackgroundColor
}
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
var menu:NSMenu!
var pullDwn:NSPopUpButton!
var count:Int = 0
let _menuItemH : CGFloat = 20
func isDarkMode(view: NSView) -> Bool {
if #available(OSX 10.14, *) {
return view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
return false
}
@objc func myBtnAction(_ sender:Any ) {
print(pullDwn.index(of:sender as! NSMenuItem))
}
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)
// **** NSPopUpButton with Menu **** //
let menuItems = ["10%","25%","50%","75%","100%","200%","300%","400%","800%","longertext"]
pullDwn = NSPopUpButton(frame:NSMakeRect(80, _wndH - 50, 80, 30), pullsDown:true)
pullDwn.autoresizingMask = [.maxXMargin,.minYMargin]
let menu = pullDwn.menu
for mItem in menuItems{
let menuItem = NSMenuItem()
menu?.addItem(menuItem)
menuItem.title = "Fit"
let textField = TextField(frame:NSMakeRect( 0, 0, pullDwn.frame.size.width, _menuItemH))
menuItem.view = textField
textField.alignment = .left // .left, .center, .right
textField.stringValue = mItem
textField.lineBreakMode = .byTruncatingTail
if (isDarkMode(view: textField)){
textField.textColor = .white
} else {
textField.textColor = .black
}
textField.backgroundColor = .windowBackgroundColor
textField.isEditable = false
textField.tag = count
textField.isBordered = false
textField.font = NSFont( name:"Menlo", size:14 )
count = count + 1
view.append(textField)
}
window.contentView!.addSubview (pullDwn)
// **** 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()
来源:https://stackoverflow.com/questions/64079668/nsmenuitem-with-custom-view-disappears-while-scrolling