【问题标题】:NSWindow: change positions of window buttonsNSWindow:改变窗口按钮的位置
【发布时间】:2015-07-17 02:45:26
【问题描述】:

我想伪造一个标题栏(更大并且颜色不同),所以到目前为止我的方法如下:

我在标题栏正下方添加了一个 NSView,然后使用以下代码将标题栏设置为透明:

self.window.titlebarAppearsTransparent = true
self.window.styleMask |= NSFullSizeContentViewWindowMask    

下一步,我将 NSView 子类化以添加一些绘图方法(背景等),尤其是代码,以便我可以使用完整的 NSView 来移动窗口(因此我使用此代码:https://stackoverflow.com/a/4564630/2062613 )

这是结果:

现在我要做的下一件事是在这个新的标题栏中将交通灯按钮垂直居中。我知道,我可以使用self.window.standardWindowButton(NSWindowButton.CloseButton) 访问按钮(例如)。但是更改其中一个按钮的frame.origin 没有任何效果。

如何更改按钮的 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)
}

【问题讨论】:

  • 我认为这是 Apple 内部的巫术。我似乎记得曾经看过 iTunes 中的红绿灯是垂直排列的。
  • 可能是这样的:找到最初包含按钮的视图。使用-[NSView trackingAreas] 找到按钮的跟踪区域(我猜这3 个按钮只有一个跟踪区域)。获取其对象属性。删除该跟踪区域。为你的假标题栏添加一个新的,除了矩形之外具有相同的属性。
  • 用 button.superview.trackingAreas[0] 和 button.superview.removeTrackingArea(...) 尝试过。没有效果:(
  • 为什么不隐藏按钮并自己制作呢?只是一个想法,甚至不确定是否可能。
  • 这工作正常,除非我们进入全屏模式。

标签: swift macos cocoa appkit


【解决方案1】:

您需要添加工具栏并更改窗口属性titleVisibility。这里有更多细节NSWindow Style Showcase

let customToolbar = NSToolbar()
window?.titleVisibility = .hidden
window?.toolbar = customToolbar

【讨论】:

  • 您也可以在Interface Builder中添加工具栏,然后删除所有默认自带的工具栏项。
【解决方案2】:

Swift 4.2 版本(无工具栏)。

背后的想法:

  • 我们调整标准窗口按钮的框架而不更改超级视图。
  • 为了防止剪裁,我们需要增加标题栏的高度。这可以通过添加透明标题栏附件来实现。
  • 当窗口进入全屏模式时,我们隐藏标题栏附件。
  • 当窗口退出全屏时,我们显示标题栏附件。
  • 此外,我们需要调整在全屏模式下标准按钮旁边显示的 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
   }
}

文件 FullContentWindowController.swift

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)

【讨论】:

  • 是否有机会通过 Cocoa 实现(+ Swift 5)对此进行更新?
【解决方案3】:

我的回答涉及到来自@Vlad 和@Lupurus 的一些回答。要更改按钮位置,只需调用 NSWindow 子类中的函数 func moveButton(ofType type: NSWindow.ButtonType) 即可处理移动。

注意:在我的例子中,我只需要将按钮降低 2px。

为了处理正常情况(不是全屏),我刚刚重写了 NSWindow 的函数 func standardWindowButton(_ b: NSWindow.ButtonType) -> NSButton? 以在返回按钮之前根据需要移动按钮。

注意:更好的代码应该有一个单独的方法来计算新帧,并且存储新值将存储在其他地方

为了在从全屏返回时正确处理动画,我们需要重写 NSWindow 的func layoutIfNeeded() 方法,该方法将在从全屏返回的动画需要时调用。

我们需要将更新的帧保存在 NSWindow 中。 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 layoutIfNeeded() 方法重新计算按钮帧:


    public func windowWillExitFullScreen(_ notification: Notification) {

        self.window.closeButtonUpdatedFrame = nil
        self.window.miniaturizeButtonUpdatedFrame = nil
        self.window.zoomButtonUpdatedFrame = nil
    }

等等!

在我的测试中,此代码处理所有情况。

【讨论】:

  • 对我不起作用。更改按钮框架原点不会影响任何内容。有什么注意事项吗?
【解决方案4】:

要使工具栏更大并且窗口控件会像在 Apple Mail 或 Notes 应用程序中那样下降一点,只需将窗口标题设置为可见并使用空的窗口标题字符串:

self.window.titleVisibility = .visible
self.window.title = ""

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-14
    • 2020-09-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多