学习使用SwiftUI开发MacOS应用 - 第2节 如何制作无边框窗口及多种窗口样式

 

 

上一节我们已经学习了如何在SwiftUI 中操作窗口及传值,在我们的实际项目中,我们还需要对窗口的样式进行设置,比如无边框窗口设置,所以在这一节中我们主要重点来学习如何对窗口的相关设置。


基于 AppKit App Delegate 模式下如何设置窗口样式。
上节已经讲到,窗口的初始化定义一般都在 AppDelegate.swift 这个文件中完成。所以相关窗口的操作基本也在这个文件中修改。

第1种  取消标题栏


启动页面 AppDelegate.swift 代码如下:

import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let contentView = ContentView()
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        
        //在styleMask配置中移除 .titled 或者直接用代码移除 .titled
        window.styleMask.remove(.titled)
        window.isReleasedWhenClosed = false
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    }
    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
}

同时我们也可以 直接在 styleMask 设置中移除 .titled 即可

styleMask: [.closable, .miniaturizable, .resizable, .fullSizeContentView],

去掉标题后,关闭按钮 和 拖动窗口 就需要自己来实现了。
如果要实现拖动窗口,有一种比较简单的办法,就是 设置窗口背景区可以支持拖动。
代码如下:

 func applicationDidFinishLaunching(_ aNotification: Notification) {
        let contentView = ContentView()
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        
        //在styleMask配置中移除 .titled 或者直接用代码移除 .titled
        window.styleMask.remove(.titled)
        window.isMovableByWindowBackground=true //设置背景可以拖动窗口
        window.isReleasedWhenClosed = false
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    }

依据上面的代码,我们来模拟拖动区域代码。
启动页面 AppDelegate.swift 我们添加两个方法,enableMove disableMove 用于开启和关闭拖动。

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let contentView = ContentView()
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        
        //在styleMask配置中移除 .titled 或者直接用代码移除 .titled
        window.styleMask.remove(.titled)
        window.isReleasedWhenClosed = false
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    }
    
    @objc func enableMove() {
        print("enableMove")
        window.isMovableByWindowBackground=true;
    }
    @objc  func disableMove() {
        print("disableMove")
        window.isMovableByWindowBackground=false;
    }
    
}

接着我们在 视图页面 ContentView.swift 添加一个 HStack 用作自己定义的拖动区域 并添加 onHover 事件,同时添加一个关闭按钮


struct ContentView: View {
    var body: some View {
        GeometryReader { proxy in
            VStack{
                HStack(spacing: 10){
                    Button("X"){
                        //添加关闭按钮
                        print("close")
                        //隐藏了标题以后NSApp.keyWindow 没有被激活(我个人也不是很清楚为啥没有被激活),所以我们只能遍历窗口找到并结束
                        NSApp.windows.forEach { (win) in
                            if win.frameAutosaveName=="Main Window"{
                                win.close()
                            }
                        }
                    }
                }
                .frame(width: proxy.size.width, height: 50, alignment: .center).background(Color.blue)
                .onHover(perform: { hovering in
                    if(hovering){
                        //当鼠标覆盖区域时开启背景拖动
                        NSApp.sendAction(#selector(AppDelegate.enableMove), to: nil, from: nil)
                    }else{
                        //鼠标移开关闭背景拖动
                        NSApp.sendAction(#selector(AppDelegate.disableMove), to: nil, from: nil)
                    }
                })
                Spacer()
                Text("Hello, World!")
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                Spacer()
            }
        }
    }
}

虽然实现了基本的功能,但是这样使用窗口可能不是最佳办法,接下来我们使用另外一种方法来实现类似的功能。

后来发现 以上的实现存在一个问题,当窗口失去焦点以后,拖动没有生效。
后修改为以下代码:
创建 TopbarView.swift 文件:

import SwiftUI

extension View {
    func withTopbarView() -> some View {
        self.background(TopbarView())
    }
}

class NSTopbarView :NSView{
    public override func mouseDown(with event: NSEvent) {
        self.window!.performDrag(with: event)
        super.mouseDown(with: event)
    }
}
struct TopbarView :NSViewRepresentable{
    func makeNSView(context: Context) -> NSTopbarView {
        return NSTopbarView()
    }
    func updateNSView(_ nsView: NSTopbarView, context: Context) {}
}

启动页面 AppDelegate.swift 改回最开始的样子

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let contentView = ContentView().frame(width: 480, height: 300, alignment: .center)
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.isReleasedWhenClosed = false
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    }
}

在需要的元素上添加修饰

struct ContentView: View {
    var body: some View {
        GeometryReader { proxy in
            VStack{
                HStack(spacing: 10){
                    Spacer()
                    Button("X"){
                        //隐藏了标题以后NSApp.keyWindow 没有被激活(我个人也不是很清楚为啥没有被激活),所以我们只能遍历窗口找到并结束
                        NSApp.windows.forEach { (win) in
                            if win.frameAutosaveName=="Main Window"{
                                win.close()
                            }
                        }
                    }
                    Spacer().frame(width: 10, height:50, alignment: .trailing)
                }
                .frame(width: proxy.size.width, height: 50, alignment: .center)
                //添加修饰 支持拖动窗口。
                .withTopbarView()
                
                .background(Color.blue)
                Spacer()
                Text("Hello, World!").frame(maxWidth: .infinity, maxHeight: .infinity)
                Spacer()
            }
        }
    }
}

最终效果:

第2种  隐藏标题栏,并设置为透明,
这样可以保留标题的相关信息和保留窗口拖动的功能。


启动页面 AppDelegate.swift 代码如下

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let contentView = ContentView()
            .background(Color.white)
            .edgesIgnoringSafeArea(.top) //取消顶部计算
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.titlebarAppearsTransparent = true //设置标题栏显示为透明
        window.titleVisibility = .hidden //设置标题不显示
        window.isReleasedWhenClosed = false
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
        
        //另隐藏按钮的设置
        window!.standardWindowButton(NSWindow.ButtonType.zoomButton)!.isHidden = true //隐藏放大按钮
        window!.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)!.isHidden = true //隐藏缩小按钮
    }

}


窗口效果:

1 毛玻璃背景
要制作毛玻璃效果,需要先创建一个视图结构 NSViewRepresentable



创建文件  VisualEffectView.swift

import SwiftUI

struct VisualEffectView: NSViewRepresentable
{
    let material: NSVisualEffectView.Material
    let blendingMode: NSVisualEffectView.BlendingMode
    
    func makeNSView(context: Context) -> NSVisualEffectView
    {
        let visualEffectView = NSVisualEffectView()
        visualEffectView.material = material
        visualEffectView.blendingMode = blendingMode
        visualEffectView.state = NSVisualEffectView.State.active
        visualEffectView.layer?.cornerRadius=0
        return visualEffectView
    }

    func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context)
    {
        visualEffectView.material = material
        visualEffectView.blendingMode = blendingMode
    }
}

启动页面 AppDelegate.swift  还需要设置窗口背景透明

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let contentView = ContentView()
            //设置视图背景效果
            .background(VisualEffectView(
                            material: NSVisualEffectView.Material.titlebar,
                            blendingMode: NSVisualEffectView.BlendingMode.withinWindow))
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled,.closable, .miniaturizable, .resizable,.fullSizeContentView],
            backing: .buffered, defer: false)
        
        window.isReleasedWhenClosed = false
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        //设置窗口背景透明
        //window.contentView?.wantsLayer=true
        //设置窗口背景透明
        window.backgroundColor = .clear
        window.makeKeyAndOrderFront(nil)
    }
}

效果2:

代码:
 

  let contentView = ContentView()
            .background(VisualEffectView(
                            material: NSVisualEffectView.Material.fullScreenUI,
                            blendingMode: NSVisualEffectView.BlendingMode.withinWindow))

可尝试修改不同的 material:参数设置不同的效果。

 

另记:

在某些情况下 窗口没有激活,keyWindow 可能会为nil  使用下面的方法可获得对应的窗口
获得当前视图所属 window 窗口中的另一种方法:

添加文件 HostingWindowFinder.swift

import SwiftUI
//扩展 View 视图的一个设置
extension View {
    func withHostingWindow(_ callback: @escaping (NSWindow?) -> Void) -> some View {
        self.background(HostingWindowFinder(callback: callback))
    }
}
//创建一个视同,用于获取窗口
struct HostingWindowFinder: NSViewRepresentable {
    var callback: (NSWindow?) -> ()
    func makeNSView(context: Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {
    }
}

在页面中使用:

struct ContentView: View {
    //设置一个窗口变量,用于存储窗口
    @State var window:NSWindow!
    var body: some View {
        GeometryReader { proxy in
            VStack{
                Text("Hello, World!").frame(maxWidth: .infinity, maxHeight: 50)
                Button("关闭窗口"){
                    window.close()
                }
                Spacer()
            }
        }.withHostingWindow { (win) in
           //设置窗口变量值
            window = win
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值