You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

macOS SwiftUI中自定义窗口控制按钮(traffic light)位置的实现方法

macOS SwiftUI中自定义窗口控制按钮(traffic light)位置的实现方法

嘿,这个需求我之前也折腾过!要在SwiftUI里把窗口的红绿灯按钮像Chrome/FireFox那样偏移位置,不用工具栏、NavigationSplitView这类组件,核心就是绕开系统默认的标题栏布局,自己接管顶部区域的控制权。下面给你一步步拆解具体实现:

1. 隐藏系统默认标题栏与控制按钮

首先得把系统自带的标题栏和红绿灯按钮藏起来,让我们的自定义内容能顶到窗口最上方。在App的入口里做这些设置:

import SwiftUI

@main
struct CustomTrafficLightDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(minWidth: 600, minHeight: 400)
                .onAppear {
                    guard let window = NSApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return }
                    
                    // 移除系统标题栏样式
                    window.styleMask.remove(.titleBar)
                    // 让标题栏区域透明,允许内容延伸到顶部
                    window.titlebarAppearsTransparent = true
                    window.titleVisibility = .hidden
                    // 取消顶部的系统边框厚度,避免内容被挤压
                    window.setContentBorderThickness(0, for: .minY)
                }
        }
    }
}

这段代码的关键是:

  • 移除.titleBar样式,彻底隐藏系统标题栏
  • 开启标题栏透明和标题隐藏,让我们的内容能铺满窗口顶部
  • 取消顶部边框厚度,确保自定义按钮不会被系统默认的间距影响

2. 自定义红绿灯按钮组件

接下来要模拟系统的三个控制按钮(关闭、最小化、最大化),还要还原它们的交互逻辑。写一个CustomTrafficLights组件:

struct CustomTrafficLights: View {
    // 获取当前激活的窗口
    private var activeWindow: NSWindow? {
        NSApplication.shared.windows.first(where: { $0.isKeyWindow })
    }
    
    var body: some View {
        HStack(spacing: 8) {
            // 关闭按钮
            Button(action: { activeWindow?.performClose(nil) }) {
                Circle()
                    .fill(Color(NSColor.systemRed))
                    .frame(width: 12, height: 12)
            }
            .buttonStyle(PlainButtonStyle())
            .hoverEffect(.highlight)
            .scaleEffect(NSApplication.shared.currentEvent?.type == .leftMouseDown ? 0.9 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: NSApplication.shared.currentEvent?.type)
            
            // 最小化按钮
            Button(action: { activeWindow?.miniaturize(nil) }) {
                Circle()
                    .fill(Color(NSColor.systemYellow))
                    .frame(width: 12, height: 12)
            }
            .buttonStyle(PlainButtonStyle())
            .hoverEffect(.highlight)
            .scaleEffect(NSApplication.shared.currentEvent?.type == .leftMouseDown ? 0.9 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: NSApplication.shared.currentEvent?.type)
            
            // 最大化/还原按钮
            Button(action: {
                guard let window = activeWindow else { return }
                window.isZoomed ? window.unzoom(nil) : window.zoom(nil)
            }) {
                Circle()
                    .fill(Color(NSColor.systemGreen))
                    .frame(width: 12, height: 12)
            }
            .buttonStyle(PlainButtonStyle())
            .hoverEffect(.highlight)
            .scaleEffect(NSApplication.shared.currentEvent?.type == .leftMouseDown ? 0.9 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: NSApplication.shared.currentEvent?.type)
        }
        // 这里的padding就是控制偏移的核心!
        .padding(.top, 10)   // 向下偏移10pt,模拟Chrome的位置
        .padding(.leading, 18) // 向右偏移18pt,比系统默认的8pt多10pt
    }
}

这里做了这些细节优化:

  • PlainButtonStyle去掉系统默认的按钮边框,只保留圆形样式
  • 加了hoverEffect和点击时的缩放动画,还原系统按钮的交互反馈
  • 通过padding直接控制按钮的偏移量,想调位置改这两个数值就行

3. 添加窗口拖拽功能(重要!)

因为我们隐藏了系统标题栏,窗口默认不能拖拽了,得自己加一个透明的拖拽区域,模拟系统标题栏的拖拽功能:

struct ContentView: View {
    // 自定义拖拽区域的NSView包装器
    struct WindowDragRegion: NSViewRepresentable {
        func makeNSView(context: Context) -> NSView {
            let view = NSView()
            view.wantsLayer = true
            // 设置拖拽区域的跟踪事件
            view.addTrackingArea(NSTrackingArea(
                rect: view.bounds,
                options: [.activeInKeyWindow, .mouseEnteredAndExited],
                owner: view,
                userInfo: nil
            ))
            return view
        }
        
        func updateNSView(_ nsView: NSView, context: Context) {
            nsView.bounds = nsView.window?.contentView?.bounds ?? .zero
        }
    }
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            // 你的主内容区域
            Text("这是你的窗口主内容")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color(NSColor.windowBackgroundColor))
            
            // 把自定义红绿灯按钮放在最上层
            CustomTrafficLights()
            
            // 全屏透明拖拽区域(覆盖窗口顶部30pt高度,和系统标题栏高度一致)
            WindowDragRegion()
                .frame(maxWidth: .infinity, maxHeight: 30)
                .background(Color.clear)
                .onTapGesture {
                    // 触发窗口拖拽
                    NSApplication.shared.windows.first(where: { $0.isKeyWindow })?.performDrag(with: NSEvent())
                }
        }
    }
}

这个拖拽区域是透明的,覆盖窗口顶部30pt高度,用户点击这个区域就能拖拽窗口,和系统标题栏的体验一致。

最后几个注意点

  • 如果你的App有多个窗口,不要直接取windows.first,最好通过@Environment(\.window)或者绑定当前窗口的引用,避免取错窗口
  • 系统红绿灯按钮还有一些特殊行为(比如按住Option键点击最大化会进入全屏),如果需要完全模拟,得监听NSEvent的修饰键状态,这个可以根据需求再加
  • 深色/浅色模式下,按钮颜色会自动跟随系统的systemRed/systemYellow/systemGreen,不用额外处理
  • 测试时要注意窗口的各种状态(比如全屏、分屏),可能需要微调padding数值适配

这样折腾完,你的窗口红绿灯按钮就能完美复刻Chrome/FireFox的偏移效果了,完全不用依赖工具栏之类的组件。如果还有细节要调,随时问哈!

火山引擎 最新活动