如何绘制自己的 NSTabView 选项卡?

发布于 2024-07-23 11:56:55 字数 99 浏览 7 评论 0 原文

我想为 NSTabViewItem 绘制自己的选项卡。 我的选项卡应该看起来不同,并且从左上角开始而不是居中。

我怎样才能做到这一点?

I want to draw my own tabs for NSTabViewItems. My Tabs should look different and start in the top left corner and not centered.

How can I do this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

渡你暖光 2024-07-30 11:56:56

可以将 NSTabView 的样式设置为 Tabless,然后使用 NSSegmentedControl 控制它,该 NSSegmentedControl 是 NSSegmentedCell 的子类以覆盖样式和行为。 要了解如何执行此操作,请查看这个模拟 Xcode 4 样式选项卡的项目:https://github.com /aaroncrespo/WILLTabView/

it is possible to set the NSTabView's style to Tabless and then control it with a NSSegmentedControl that subclasses NSSegmentedCell to override style and behavior. For an idea how to do this, check out this project that emulates Xcode 4 style tabs: https://github.com/aaroncrespo/WILLTabView/.

山人契 2024-07-30 11:56:56

绘制选项卡的可能方法之一是使用 NSCollectionView。 下面是 Swift 4 示例:

TabViewStackController 包含使用 .unspecified 样式和自定义 TabBarView 预配置的 TabViewController

class TabViewStackController: ViewController {

   private lazy var tabBarView = TabBarView().autolayoutView()
   private lazy var containerView = View().autolayoutView()
   private lazy var tabViewController = TabViewController()
   private let tabs: [String] = (0 ..< 14).map { "TabItem # \($0)" }

   override func setupUI() {
      view.addSubviews(tabBarView, containerView)
      embedChildViewController(tabViewController, container: containerView)
   }

   override func setupLayout() {
      LayoutConstraint.withFormat("|-[*]-|", forEveryViewIn: containerView, tabBarView).activate()
      LayoutConstraint.withFormat("V:|-[*]-[*]-|", tabBarView, containerView).activate()
   }

   override func setupHandlers() {
      tabBarView.eventHandler = { [weak self] in
         switch $0 {
         case .select(let item):
            self?.tabViewController.process(item: item)
         }
      }
   }

   override func setupDefaults() {
      tabBarView.tabs = tabs
      if let item = tabs.first {
         tabBarView.select(item: item)
         tabViewController.process(item: item)
      }
   }
}

TabBarView 类包含表示选项卡的 CollectionView

class TabBarView: View {

   public enum Event {
      case select(String)
   }

   public var eventHandler: ((Event) -> Void)?

   private let cellID = NSUserInterfaceItemIdentifier(rawValue: "cid.tabView")
   public var tabs: [String] = [] {
      didSet {
         collectionView.reloadData()
      }
   }

   private lazy var collectionView = TabBarCollectionView()
   private let tabBarHeight: CGFloat = 28
   private (set) lazy var scrollView = TabBarScrollView(collectionView: collectionView).autolayoutView()

   override var intrinsicContentSize: NSSize {
      let size = CGSize(width: NSView.noIntrinsicMetric, height: tabBarHeight)
      return size
   }

   override func setupHandlers() {
      collectionView.delegate = self
   }

   override func setupDataSource() {
      collectionView.dataSource = self
      collectionView.register(TabBarTabViewItem.self, forItemWithIdentifier: cellID)
   }

   override func setupUI() {
      addSubviews(scrollView)

      wantsLayer = true

      let gridLayout = NSCollectionViewGridLayout()
      gridLayout.maximumNumberOfRows = 1
      gridLayout.minimumItemSize = CGSize(width: 115, height: tabBarHeight)
      gridLayout.maximumItemSize = gridLayout.minimumItemSize
      collectionView.collectionViewLayout = gridLayout
   }

   override func setupLayout() {
      LayoutConstraint.withFormat("|[*]|", scrollView).activate()
      LayoutConstraint.withFormat("V:|[*]|", scrollView).activate()
   }
}

extension TabBarView {

   func select(item: String) {
      if let index = tabs.index(of: item) {
         let ip = IndexPath(item: index, section: 0)
         if collectionView.item(at: ip) != nil {
            collectionView.selectItems(at: [ip], scrollPosition: [])
         }
      }
   }
}

extension TabBarView: NSCollectionViewDataSource {

   func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
      return tabs.count
   }

   func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
      let tabItem = tabs[indexPath.item]
      let cell = collectionView.makeItem(withIdentifier: cellID, for: indexPath)
      if let cell = cell as? TabBarTabViewItem {
         cell.configure(title: tabItem)
      }
      return cell
   }
}

extension TabBarView: NSCollectionViewDelegate {

   func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
      if let first = indexPaths.first {
         let item = tabs[first.item]
         eventHandler?(.select(item))
      }
   }
}

TabViewController 预配置了样式 .unspecified

class TabViewController: GenericTabViewController<String> {

   override func viewDidLoad() {
      super.viewDidLoad()
      transitionOptions = []
      tabStyle = .unspecified
   }

   func process(item: String) {
      if index(of: item) != nil {
         select(itemIdentifier: item)
      } else {
         let vc = TabContentController(content: item)
         let tabItem = GenericTabViewItem(identifier: item, viewController: vc)
         addTabViewItem(tabItem)
         select(itemIdentifier: item)
      }
   }
}

其余类。

class TabBarCollectionView: CollectionView {

   override func setupUI() {
      isSelectable = true
      allowsMultipleSelection = false
      allowsEmptySelection = false
      backgroundView = View(backgroundColor: .magenta)
      backgroundColors = [.clear]
   }
}

class TabBarScrollView: ScrollView {

   override func setupUI() {
      borderType = .noBorder
      backgroundColor = .clear
      drawsBackground = false

      horizontalScrollElasticity = .none
      verticalScrollElasticity = .none

      automaticallyAdjustsContentInsets = false
      horizontalScroller = InvisibleScroller()
   }
}

// Disabling scroll view indicators.
// See: https://stackoverflow.com/questions/9364953/hide-scrollers-while-leaving-scrolling-itself-enabled-in-nsscrollview
private class InvisibleScroller: Scroller {

   override class var isCompatibleWithOverlayScrollers: Bool {
      return true
   }

   override class func scrollerWidth(for controlSize: NSControl.ControlSize, scrollerStyle: NSScroller.Style) -> CGFloat {
      return CGFloat.leastNormalMagnitude // Dimension of scroller is equal to `FLT_MIN`
   }

   override func setupUI() {
      // Below assignments not really needed, but why not.
      scrollerStyle = .overlay
      alphaValue = 0
   }
}

class TabBarTabViewItem: CollectionViewItem {

   private lazy var titleLabel = Label().autolayoutView()

   override var isSelected: Bool {
      didSet {
         if isSelected {
            titleLabel.font = Font.semibold(size: 10)
            contentView.backgroundColor = .red
         } else {
            titleLabel.font = Font.regular(size: 10.2)
            contentView.backgroundColor = .blue
         }
      }
   }

   override func setupUI() {
      view.addSubviews(titleLabel)
      view.wantsLayer = true
      titleLabel.maximumNumberOfLines = 1
   }

   override func setupDefaults() {
      isSelected = false
   }

   func configure(title: String) {
      titleLabel.text = title
      titleLabel.textColor = .white
      titleLabel.alignment = .center
   }

   override func setupLayout() {
      LayoutConstraint.withFormat("|-[*]-|", titleLabel).activate()
      LayoutConstraint.withFormat("V:|-(>=4)-[*]", titleLabel).activate()
      LayoutConstraint.centerY(titleLabel).activate()
   }
}

class TabContentController: ViewController {

   let content: String
   private lazy var titleLabel = Label().autolayoutView()

   init(content: String) {
      self.content = content
      super.init()
   }

   required init?(coder: NSCoder) {
      fatalError()
   }

   override func setupUI() {
      contentView.addSubview(titleLabel)
      titleLabel.text = content
      contentView.backgroundColor = .green
   }

   override func setupLayout() {
      LayoutConstraint.centerXY(titleLabel).activate()
   }
}

其外观如下:

One of possible ways to draw tabs - is to use NSCollectionView. Here is Swift 4 example:

Class TabViewStackController contains TabViewController preconfigured with style .unspecified and custom TabBarView.

class TabViewStackController: ViewController {

   private lazy var tabBarView = TabBarView().autolayoutView()
   private lazy var containerView = View().autolayoutView()
   private lazy var tabViewController = TabViewController()
   private let tabs: [String] = (0 ..< 14).map { "TabItem # \($0)" }

   override func setupUI() {
      view.addSubviews(tabBarView, containerView)
      embedChildViewController(tabViewController, container: containerView)
   }

   override func setupLayout() {
      LayoutConstraint.withFormat("|-[*]-|", forEveryViewIn: containerView, tabBarView).activate()
      LayoutConstraint.withFormat("V:|-[*]-[*]-|", tabBarView, containerView).activate()
   }

   override func setupHandlers() {
      tabBarView.eventHandler = { [weak self] in
         switch $0 {
         case .select(let item):
            self?.tabViewController.process(item: item)
         }
      }
   }

   override func setupDefaults() {
      tabBarView.tabs = tabs
      if let item = tabs.first {
         tabBarView.select(item: item)
         tabViewController.process(item: item)
      }
   }
}

Class TabBarView contains CollectionView which represents tabs.

class TabBarView: View {

   public enum Event {
      case select(String)
   }

   public var eventHandler: ((Event) -> Void)?

   private let cellID = NSUserInterfaceItemIdentifier(rawValue: "cid.tabView")
   public var tabs: [String] = [] {
      didSet {
         collectionView.reloadData()
      }
   }

   private lazy var collectionView = TabBarCollectionView()
   private let tabBarHeight: CGFloat = 28
   private (set) lazy var scrollView = TabBarScrollView(collectionView: collectionView).autolayoutView()

   override var intrinsicContentSize: NSSize {
      let size = CGSize(width: NSView.noIntrinsicMetric, height: tabBarHeight)
      return size
   }

   override func setupHandlers() {
      collectionView.delegate = self
   }

   override func setupDataSource() {
      collectionView.dataSource = self
      collectionView.register(TabBarTabViewItem.self, forItemWithIdentifier: cellID)
   }

   override func setupUI() {
      addSubviews(scrollView)

      wantsLayer = true

      let gridLayout = NSCollectionViewGridLayout()
      gridLayout.maximumNumberOfRows = 1
      gridLayout.minimumItemSize = CGSize(width: 115, height: tabBarHeight)
      gridLayout.maximumItemSize = gridLayout.minimumItemSize
      collectionView.collectionViewLayout = gridLayout
   }

   override func setupLayout() {
      LayoutConstraint.withFormat("|[*]|", scrollView).activate()
      LayoutConstraint.withFormat("V:|[*]|", scrollView).activate()
   }
}

extension TabBarView {

   func select(item: String) {
      if let index = tabs.index(of: item) {
         let ip = IndexPath(item: index, section: 0)
         if collectionView.item(at: ip) != nil {
            collectionView.selectItems(at: [ip], scrollPosition: [])
         }
      }
   }
}

extension TabBarView: NSCollectionViewDataSource {

   func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
      return tabs.count
   }

   func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
      let tabItem = tabs[indexPath.item]
      let cell = collectionView.makeItem(withIdentifier: cellID, for: indexPath)
      if let cell = cell as? TabBarTabViewItem {
         cell.configure(title: tabItem)
      }
      return cell
   }
}

extension TabBarView: NSCollectionViewDelegate {

   func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
      if let first = indexPaths.first {
         let item = tabs[first.item]
         eventHandler?(.select(item))
      }
   }
}

Class TabViewController preconfigured with style .unspecified

class TabViewController: GenericTabViewController<String> {

   override func viewDidLoad() {
      super.viewDidLoad()
      transitionOptions = []
      tabStyle = .unspecified
   }

   func process(item: String) {
      if index(of: item) != nil {
         select(itemIdentifier: item)
      } else {
         let vc = TabContentController(content: item)
         let tabItem = GenericTabViewItem(identifier: item, viewController: vc)
         addTabViewItem(tabItem)
         select(itemIdentifier: item)
      }
   }
}

Rest of the classes.

class TabBarCollectionView: CollectionView {

   override func setupUI() {
      isSelectable = true
      allowsMultipleSelection = false
      allowsEmptySelection = false
      backgroundView = View(backgroundColor: .magenta)
      backgroundColors = [.clear]
   }
}

class TabBarScrollView: ScrollView {

   override func setupUI() {
      borderType = .noBorder
      backgroundColor = .clear
      drawsBackground = false

      horizontalScrollElasticity = .none
      verticalScrollElasticity = .none

      automaticallyAdjustsContentInsets = false
      horizontalScroller = InvisibleScroller()
   }
}

// Disabling scroll view indicators.
// See: https://stackoverflow.com/questions/9364953/hide-scrollers-while-leaving-scrolling-itself-enabled-in-nsscrollview
private class InvisibleScroller: Scroller {

   override class var isCompatibleWithOverlayScrollers: Bool {
      return true
   }

   override class func scrollerWidth(for controlSize: NSControl.ControlSize, scrollerStyle: NSScroller.Style) -> CGFloat {
      return CGFloat.leastNormalMagnitude // Dimension of scroller is equal to `FLT_MIN`
   }

   override func setupUI() {
      // Below assignments not really needed, but why not.
      scrollerStyle = .overlay
      alphaValue = 0
   }
}

class TabBarTabViewItem: CollectionViewItem {

   private lazy var titleLabel = Label().autolayoutView()

   override var isSelected: Bool {
      didSet {
         if isSelected {
            titleLabel.font = Font.semibold(size: 10)
            contentView.backgroundColor = .red
         } else {
            titleLabel.font = Font.regular(size: 10.2)
            contentView.backgroundColor = .blue
         }
      }
   }

   override func setupUI() {
      view.addSubviews(titleLabel)
      view.wantsLayer = true
      titleLabel.maximumNumberOfLines = 1
   }

   override func setupDefaults() {
      isSelected = false
   }

   func configure(title: String) {
      titleLabel.text = title
      titleLabel.textColor = .white
      titleLabel.alignment = .center
   }

   override func setupLayout() {
      LayoutConstraint.withFormat("|-[*]-|", titleLabel).activate()
      LayoutConstraint.withFormat("V:|-(>=4)-[*]", titleLabel).activate()
      LayoutConstraint.centerY(titleLabel).activate()
   }
}

class TabContentController: ViewController {

   let content: String
   private lazy var titleLabel = Label().autolayoutView()

   init(content: String) {
      self.content = content
      super.init()
   }

   required init?(coder: NSCoder) {
      fatalError()
   }

   override func setupUI() {
      contentView.addSubview(titleLabel)
      titleLabel.text = content
      contentView.backgroundColor = .green
   }

   override func setupLayout() {
      LayoutConstraint.centerXY(titleLabel).activate()
   }
}

Here is how it looks like:

Result

┈┾☆殇 2024-07-30 11:56:56

NSTabView 不是 Cocoa 中最可定制的类,但可以对其进行子类化并进行自己的绘图。 除了维护选项卡视图项的集合之外,您不会使用超类的太多功能,并且最终将实现许多 NSView 和 NSResponder 方法以使绘图和事件处理正常工作。

最好先看看免费或开源选项卡栏控件之一,我使用过 PSMTabBarControl 在过去,它比实现我自己的选项卡视图子类(这是它所取代的)要容易得多。

NSTabView isn't the most customizable class in Cocoa, but it is possible to subclass it and do your own drawing. You won't use much functionality from the superclass besides maintaining a collection of tab view items, and you'll end up implementing a number of NSView and NSResponder methods to get the drawing and event handling working correctly.

It might be best to look at one of the free or open source tab bar controls first, I've used PSMTabBarControl in the past, and it was much easier than implementing my own tab view subclass (which is what it was replacing).

许你一世情深 2024-07-30 11:56:56

我最近为我正在做的事情做了这个。

我最终使用了 tabless 选项卡视图,然后自己在另一个视图中绘制选项卡。 我希望我的选项卡成为窗口底部状态栏的一部分。

显然,您需要支持鼠标单击,这相当容易,但是您应该确保键盘支持也能正常工作,这有点棘手:您需要运行计时器来在半秒后没有键盘访问后切换选项卡(看看 OS X 是如何做的)。 可访问性是您应该考虑的另一件事,但您可能会发现它确实有效 - 我还没有在我的代码中检查它。

I've recently done this for something I was working on.

I ended using a tabless tab view and then drawing the tabs myself in another view. I wanted my tabs to be part of a status bar at the bottom of the window.

You obviously need to support mouse clicks which is fairly easy, but you should make sure your keyboard support works too, and that's a little more tricky: you'll need to run timers to switch the tab after no keyboard access after half a second (have a look at the way OS X does it). Accessibility is another thing you should think about but you might find it just works—I haven't checked it in my code yet.

独守阴晴ぅ圆缺 2024-07-30 11:56:56

我对此非常困惑 - 并发布了 带有背景颜色的 NSTabView - 因为 PSMTabBarControl 现在是过时的还发布了 https://github.com/dirkx/CustomizedTabView /blob/master/CustomizedTabView/CustomizedTabView.m

I very much got stuck on this - and posted NSTabView with background color - as the PSMTabBarControl is now out of date also posted https://github.com/dirkx/CustomizableTabView/blob/master/CustomizableTabView/CustomizableTabView.m

三生池水覆流年 2024-07-30 11:56:56

使用单独的 NSSegmentedCell 来控制 NSTabView 中的选项卡选择非常容易。 您所需要的只是一个它们都可以绑定到的实例变量,无论是在文件所有者中,还是在 nib 文件中出现的任何其他控制器类中。 只需在类 Interface 声明中添加类似的内容即可:

@property NSInteger selectedTabIndex;

然后,在 IB Bindings Inspector 中,将 NSTabViewNSSegmentedCell 的 Selected Index 绑定到同一个 selectedTabIndex 属性。

这就是您需要做的全部! 除非您希望默认选定的选项卡索引不为零,否则不需要初始化该属性。 您可以保留选项卡,也可以制作 NSTabView 表,无论哪种方式都可以。 无论哪个控件更改选择,控件都将保持同步。

It's very easy to use a separate NSSegmentedCell to control tab selection in an NSTabView. All you need is an instance variable that they can both bind to, either in the File's Owner, or any other controller class that appears in your nib file. Just put something like this in the class Interface declaraton:

@property NSInteger selectedTabIndex;

Then, in the IB Bindings Inspector, bind the Selected Index of both the NSTabView and the NSSegmentedCell to the same selectedTabIndex property.

That's all you need to do! You don't need to initialize the property unless you want the default selected tab index to be something other than zero. You can either keep the tabs, or make the NSTabView tabless, it will work either way. The controls will stay in sync regardless of which control changes the selection.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文