Swift NSWindow:更改窗口按钮的位置
我想伪造一个标题栏(更大,颜色不同),所以我现在的做法如下: 我在标题栏的正下方添加了一个NSView,然后使用以下代码将标题栏设置为透明:Swift NSWindow:更改窗口按钮的位置,swift,macos,cocoa,appkit,Swift,Macos,Cocoa,Appkit,我想伪造一个标题栏(更大,颜色不同),所以我现在的做法如下: 我在标题栏的正下方添加了一个NSView,然后使用以下代码将标题栏设置为透明: self.window.titlebarAppearsTransparent = true self.window.styleMask |= NSFullSizeContentViewWindowMask 下一步是,我将NSView子类化,以添加一些绘图方法(背景等),特别是代码,以便我可以使用完整的NSView来移动窗口(因此我使用此代码:)
self.window.titlebarAppearsTransparent = true
self.window.styleMask |= NSFullSizeContentViewWindowMask
下一步是,我将NSView子类化,以添加一些绘图方法(背景等),特别是代码,以便我可以使用完整的NSView来移动窗口(因此我使用此代码:)
结果是:
现在,我想做的下一件事是垂直居中交通灯按钮在这个新的标题栏。我知道,我可以使用self.window.standardWindowButton(NSWindowButton.CloseButton)
(例如)访问按钮。但是更改其中一个按钮的frame.origin
没有任何效果
如何更改按钮的原点.y值
更新
我发现,调整窗口大小会重新排列按钮。现在我决定将这些按钮作为子视图添加到我的假标题栏中,因为在标题栏中移动原点会切断按钮(这显然仅限于标题栏矩形)
这是可行的,但奇怪的是,按钮的鼠标悬停效果仍然保留在标题栏中。请看此屏幕:
这实际上是我的代码:
func moveButtons() {
self.moveButtonDownFirst(self.window.standardWindowButton(NSWindowButton.CloseButton)!)
self.moveButtonDownFirst(self.window.standardWindowButton(NSWindowButton.MiniaturizeButton)!)
self.moveButtonDownFirst(self.window.standardWindowButton(NSWindowButton.ZoomButton)!)
}
func moveButtonDownFirst(button: NSView) {
button.setFrameOrigin(NSMakePoint(button.frame.origin.x, button.frame.origin.y+10.0))
self.fakeTitleBar.addSubview(button)
}
您需要添加工具栏并更改窗口属性
titleviability
。这里有更多的细节
Swift 4.2版(不带工具栏)
背后的想法:
- 我们在不改变superview的情况下调整标准窗口按钮的框架
- 为了防止剪切,我们需要增加标题栏的高度。这可以通过添加透明标题栏附件来实现
- 当窗口转到全屏时,我们隐藏标题栏附件
- 当窗口退出全屏时,我们显示标题栏附件
- 此外,我们需要在全屏模式下调整显示在标准按钮旁边的UI元素的布局
正常屏幕。 全屏模式。 实际应用程序
文件FullContentWindow.swift
public class FullContentWindow: Window {
private var buttons: [NSButton] = []
public let titleBarAccessoryViewController = TitlebarAccessoryViewController()
private lazy var titleBarHeight = calculatedTitleBarHeight
private let titleBarLeadingOffset: CGFloat?
private var originalLeadingOffsets: [CGFloat] = []
public init(contentRect: NSRect, titleBarHeight: CGFloat, titleBarLeadingOffset: CGFloat? = nil) {
self.titleBarLeadingOffset = titleBarLeadingOffset
let styleMask: NSWindow.StyleMask = [.closable, .titled, .miniaturizable, .resizable, .fullSizeContentView]
super.init(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: true)
titleVisibility = .hidden
titlebarAppearsTransparent = true
buttons = [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton].compactMap {
standardWindowButton($0)
}
var accessoryViewHeight = titleBarHeight - calculatedTitleBarHeight
accessoryViewHeight = max(0, accessoryViewHeight)
titleBarAccessoryViewController.view.frame = CGRect(dimension: accessoryViewHeight) // Width not used.
if accessoryViewHeight > 0 {
addTitlebarAccessoryViewController(titleBarAccessoryViewController)
}
self.titleBarHeight = max(titleBarHeight, calculatedTitleBarHeight)
}
public override func layoutIfNeeded() {
super.layoutIfNeeded()
if originalLeadingOffsets.isEmpty {
let firstButtonOffset = buttons.first?.frame.origin.x ?? 0
originalLeadingOffsets = buttons.map { $0.frame.origin.x - firstButtonOffset }
}
if titleBarAccessoryViewController.view.frame.height > 0, !titleBarAccessoryViewController.isHidden {
setupButtons()
}
}
}
extension FullContentWindow {
public var standardWindowButtonsRect: CGRect {
var result = CGRect()
if let firstButton = buttons.first, let lastButton = buttons.last {
let leadingOffset = firstButton.frame.origin.x
let maxX = lastButton.frame.maxX
result = CGRect(x: leadingOffset, y: 0, width: maxX - leadingOffset, height: titleBarHeight)
if let titleBarLeadingOffset = titleBarLeadingOffset {
result = result.offsetBy(dx: titleBarLeadingOffset - leadingOffset, dy: 0)
}
}
return result
}
}
extension FullContentWindow {
private func setupButtons() {
let barHeight = calculatedTitleBarHeight
for (idx, button) in buttons.enumerated() {
let coordY = (barHeight - button.frame.size.height) * 0.5
var coordX = button.frame.origin.x
if let titleBarLeadingOffset = titleBarLeadingOffset {
coordX = titleBarLeadingOffset + originalLeadingOffsets[idx]
}
button.setFrameOrigin(CGPoint(x: coordX, y: coordY))
}
}
private var calculatedTitleBarHeight: CGFloat {
let result = contentRect(forFrameRect: frame).height - contentLayoutRect.height
return result
}
}
open class FullContentWindowController: WindowController {
private let fullContentWindow: FullContentWindow
private let fullContentViewController = ViewController()
public private (set) lazy var titleBarContentContainer = View().autolayoutView()
public private (set) lazy var contentContainer = View().autolayoutView()
private lazy var titleOffsetConstraint =
titleBarContentContainer.leadingAnchor.constraint(equalTo: fullContentViewController.contentView.leadingAnchor)
public init(contentRect: CGRect, titleBarHeight: CGFloat, titleBarLeadingOffset: CGFloat? = nil) {
fullContentWindow = FullContentWindow(contentRect: contentRect, titleBarHeight: titleBarHeight,
titleBarLeadingOffset: titleBarLeadingOffset)
super.init(window: fullContentWindow, viewController: fullContentViewController)
contentWindow.delegate = self
fullContentViewController.contentView.addSubviews(titleBarContentContainer, contentContainer)
let standardWindowButtonsRect = fullContentWindow.standardWindowButtonsRect
LayoutConstraint.withFormat("V:|[*][*]|", titleBarContentContainer, contentContainer).activate()
LayoutConstraint.pin(to: .horizontally, contentContainer).activate()
LayoutConstraint.constrainHeight(constant: standardWindowButtonsRect.height, titleBarContentContainer).activate()
LayoutConstraint.withFormat("[*]|", titleBarContentContainer).activate()
titleOffsetConstraint.activate()
titleOffsetConstraint.constant = standardWindowButtonsRect.maxX
}
open override func prepareForInterfaceBuilder() {
titleBarContentContainer.backgroundColor = .green
contentContainer.backgroundColor = .yellow
fullContentViewController.contentView.backgroundColor = .blue
fullContentWindow.titleBarAccessoryViewController.contentView.backgroundColor = Color.red.withAlphaComponent(0.4)
}
public required init?(coder: NSCoder) {
fatalError()
}
}
extension FullContentWindowController {
public func embedTitleBarContent(_ viewController: NSViewController) {
fullContentViewController.embedChildViewController(viewController, container: titleBarContentContainer)
}
public func embedContent(_ viewController: NSViewController) {
fullContentViewController.embedChildViewController(viewController, container: contentContainer)
}
}
extension FullContentWindowController: NSWindowDelegate {
public func windowWillEnterFullScreen(_ notification: Notification) {
fullContentWindow.titleBarAccessoryViewController.isHidden = true
titleOffsetConstraint.constant = 0
}
public func windowWillExitFullScreen(_ notification: Notification) {
fullContentWindow.titleBarAccessoryViewController.isHidden = false
titleOffsetConstraint.constant = fullContentWindow.standardWindowButtonsRect.maxX
}
}
文件FullContentWindowController.swift
public class FullContentWindow: Window {
private var buttons: [NSButton] = []
public let titleBarAccessoryViewController = TitlebarAccessoryViewController()
private lazy var titleBarHeight = calculatedTitleBarHeight
private let titleBarLeadingOffset: CGFloat?
private var originalLeadingOffsets: [CGFloat] = []
public init(contentRect: NSRect, titleBarHeight: CGFloat, titleBarLeadingOffset: CGFloat? = nil) {
self.titleBarLeadingOffset = titleBarLeadingOffset
let styleMask: NSWindow.StyleMask = [.closable, .titled, .miniaturizable, .resizable, .fullSizeContentView]
super.init(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: true)
titleVisibility = .hidden
titlebarAppearsTransparent = true
buttons = [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton].compactMap {
standardWindowButton($0)
}
var accessoryViewHeight = titleBarHeight - calculatedTitleBarHeight
accessoryViewHeight = max(0, accessoryViewHeight)
titleBarAccessoryViewController.view.frame = CGRect(dimension: accessoryViewHeight) // Width not used.
if accessoryViewHeight > 0 {
addTitlebarAccessoryViewController(titleBarAccessoryViewController)
}
self.titleBarHeight = max(titleBarHeight, calculatedTitleBarHeight)
}
public override func layoutIfNeeded() {
super.layoutIfNeeded()
if originalLeadingOffsets.isEmpty {
let firstButtonOffset = buttons.first?.frame.origin.x ?? 0
originalLeadingOffsets = buttons.map { $0.frame.origin.x - firstButtonOffset }
}
if titleBarAccessoryViewController.view.frame.height > 0, !titleBarAccessoryViewController.isHidden {
setupButtons()
}
}
}
extension FullContentWindow {
public var standardWindowButtonsRect: CGRect {
var result = CGRect()
if let firstButton = buttons.first, let lastButton = buttons.last {
let leadingOffset = firstButton.frame.origin.x
let maxX = lastButton.frame.maxX
result = CGRect(x: leadingOffset, y: 0, width: maxX - leadingOffset, height: titleBarHeight)
if let titleBarLeadingOffset = titleBarLeadingOffset {
result = result.offsetBy(dx: titleBarLeadingOffset - leadingOffset, dy: 0)
}
}
return result
}
}
extension FullContentWindow {
private func setupButtons() {
let barHeight = calculatedTitleBarHeight
for (idx, button) in buttons.enumerated() {
let coordY = (barHeight - button.frame.size.height) * 0.5
var coordX = button.frame.origin.x
if let titleBarLeadingOffset = titleBarLeadingOffset {
coordX = titleBarLeadingOffset + originalLeadingOffsets[idx]
}
button.setFrameOrigin(CGPoint(x: coordX, y: coordY))
}
}
private var calculatedTitleBarHeight: CGFloat {
let result = contentRect(forFrameRect: frame).height - contentLayoutRect.height
return result
}
}
open class FullContentWindowController: WindowController {
private let fullContentWindow: FullContentWindow
private let fullContentViewController = ViewController()
public private (set) lazy var titleBarContentContainer = View().autolayoutView()
public private (set) lazy var contentContainer = View().autolayoutView()
private lazy var titleOffsetConstraint =
titleBarContentContainer.leadingAnchor.constraint(equalTo: fullContentViewController.contentView.leadingAnchor)
public init(contentRect: CGRect, titleBarHeight: CGFloat, titleBarLeadingOffset: CGFloat? = nil) {
fullContentWindow = FullContentWindow(contentRect: contentRect, titleBarHeight: titleBarHeight,
titleBarLeadingOffset: titleBarLeadingOffset)
super.init(window: fullContentWindow, viewController: fullContentViewController)
contentWindow.delegate = self
fullContentViewController.contentView.addSubviews(titleBarContentContainer, contentContainer)
let standardWindowButtonsRect = fullContentWindow.standardWindowButtonsRect
LayoutConstraint.withFormat("V:|[*][*]|", titleBarContentContainer, contentContainer).activate()
LayoutConstraint.pin(to: .horizontally, contentContainer).activate()
LayoutConstraint.constrainHeight(constant: standardWindowButtonsRect.height, titleBarContentContainer).activate()
LayoutConstraint.withFormat("[*]|", titleBarContentContainer).activate()
titleOffsetConstraint.activate()
titleOffsetConstraint.constant = standardWindowButtonsRect.maxX
}
open override func prepareForInterfaceBuilder() {
titleBarContentContainer.backgroundColor = .green
contentContainer.backgroundColor = .yellow
fullContentViewController.contentView.backgroundColor = .blue
fullContentWindow.titleBarAccessoryViewController.contentView.backgroundColor = Color.red.withAlphaComponent(0.4)
}
public required init?(coder: NSCoder) {
fatalError()
}
}
extension FullContentWindowController {
public func embedTitleBarContent(_ viewController: NSViewController) {
fullContentViewController.embedChildViewController(viewController, container: titleBarContentContainer)
}
public func embedContent(_ viewController: NSViewController) {
fullContentViewController.embedChildViewController(viewController, container: contentContainer)
}
}
extension FullContentWindowController: NSWindowDelegate {
public func windowWillEnterFullScreen(_ notification: Notification) {
fullContentWindow.titleBarAccessoryViewController.isHidden = true
titleOffsetConstraint.constant = 0
}
public func windowWillExitFullScreen(_ notification: Notification) {
fullContentWindow.titleBarAccessoryViewController.isHidden = false
titleOffsetConstraint.constant = fullContentWindow.standardWindowButtonsRect.maxX
}
}
用法
let windowController = FullContentWindowController(contentRect: CGRect(...),
titleBarHeight: 30,
titleBarLeadingOffset: 7)
windowController.embedContent(viewController) // Content "Yellow area"
windowController.embedTitleBarContent(titleBarController) // Titlebar "Green area"
windowController.showWindow(nil)
我的回答包含了一些来自@Vlad和@Lupurus的答案。要更改按钮位置,只需调用NSWindow子类中的函数
func moveButton(类型:NSWindow.ButtonType)
,即可处理移动操作
注意:在我的情况下,我只需要按钮降低一点2px
为了处理正常情况(非全屏),我刚刚重写了NSWindow的函数func standardWindowButton(b:NSWindow.ButtonType)->NSButton?
,以便在返回按钮之前根据需要移动按钮
注意:更好的代码应该有一个单独的方法来计算新的帧,存储新的值将存储在其他地方
为了在从全屏返回时正确处理动画,我们需要覆盖NSWindow的func layoutifneed()
方法,当从全屏返回的动画需要时,将调用此方法
我们需要在窗口中保存更新的帧。nil值将触发帧重新计算
您需要在NSWindow中保留更新的帧:
var closeButtonUpdatedFrame: NSRect?
var miniaturizeButtonUpdatedFrame: NSRect?
var zoomButtonUpdatedFrame: NSRect?
public override func layoutIfNeeded() {
super.layoutIfNeeded()
if closeButtonUpdatedFrame == nil {
moveButton(ofType: .closeButton)
}
if miniaturizeButtonUpdatedFrame == nil {
moveButton(ofType: .miniaturizeButton)
}
if zoomButtonUpdatedFrame == nil {
moveButton(ofType: .zoomButton)
}
}
override public func standardWindowButton(_ b: NSWindow.ButtonType) -> NSButton? {
switch b {
case .closeButton:
if closeButtonUpdatedFrame == nil {
moveButton(ofType: b)
}
case .miniaturizeButton:
if miniaturizeButtonUpdatedFrame == nil {
moveButton(ofType: b)
}
case .zoomButton:
if zoomButtonUpdatedFrame == nil {
moveButton(ofType: b)
}
default:
break
}
return super.standardWindowButton(b)
}
func moveButton(ofType type: NSWindow.ButtonType) {
guard let button = super.standardWindowButton(type) else {
return
}
switch type {
case .closeButton:
self.moveButtonDown(button: button)
closeButtonUpdatedFrame = button.frame
case .miniaturizeButton:
self.moveButtonDown(button: button)
miniaturizeButtonUpdatedFrame = button.frame
case .zoomButton:
self.moveButtonDown(button: button)
zoomButtonUpdatedFrame = button.frame
default:
break
}
}
func moveButtonDown(button: NSView) {
button.setFrameOrigin(NSMakePoint(button.frame.origin.x, button.frame.origin.y-2.0))
}
要处理全屏情况,我们需要在NSWindowDelegate中放入一些代码,在我的例子中,这个委托就是NSWindowController实例。此代码将强制func layoutifneed()
方法在来自全屏时重新计算按钮帧:
公共函数窗口将退出全屏幕(\通知:通知){
self.window.closeButtonUpdatedFrame=nil
self.window.minimizeButtonUpdatedFrame=nil
self.window.zoomButtonUpdatedFrame=nil
}
瞧
在我的测试中,这段代码处理所有情况 我认为这是苹果内部的巫术。我似乎记得我看过iTunes,它的红绿灯是垂直排列的。可能是这样的:找到最初包含按钮的视图。使用
-[NSView trackingAreas]
查找按钮的跟踪区域(我猜3个按钮只有一个跟踪区域)。获取其对象属性。移除跟踪区域。为假标题栏添加一个新标题栏,除了矩形外,其他属性都相同。请使用button.superview.trackingAreas[0]和button.superview.removeTrackingArea(…)进行尝试。无效:(为什么不隐藏按钮并自己制作呢?只是一个想法,甚至不确定是否可能。除了进入全屏外,这可以正常工作。您也可以在Interface Builder中添加工具栏,然后删除默认情况下出现的所有工具栏项。对我来说不起作用。更改按钮框原点不会影响任何事情。是否有一些警告?是否有任何错误是否需要Cocoa实施(+Swift 5)的更新?