macOS屏幕NSScreen
macOS屏幕NSScreen

macOS屏幕NSScreen

NSScreen 是 macOS AppKit 框架中表示屏幕显示器(屏幕)的类,用于获取和管理用户连接的所有显示器的相关信息,比如:

每个屏幕的大小、位置、比例(DPI)、颜色空间;

哪一个是主屏幕(primary screen);

多显示器环境中每个屏幕的坐标。

基本用法

class NSScreen : NSObject

不能创建 NSScreen 实例,它由系统提供(屏幕由 macOS 管理)。可以通过 NSScreen.screens 或 NSScreen.main 获取实例,然后读取其属性:

import AppKit

let screens = NSScreen.screens // 所有屏幕(数组)
let mainScreen = NSScreen.main // 主屏幕(可能为 nil)

常用属性

1、screens: [NSScreen]

返回当前连接的所有显示器(包括主屏、副屏)。

for screen in NSScreen.screens {
    print(screen.frame)	// (0.0, 0.0, 1470.0, 956.0)
}

通常数组第一个是主屏,但不绝对(以 NSScreen.main 为准)。

2、main: NSScreen?

当前应用活动窗口所在的屏幕(并非鼠标所在屏幕)。

if let main = NSScreen.main {
    print("当前屏幕尺寸:\(main.frame.size)")
}

3、frame: NSRect

屏幕在全局坐标中的位置和尺寸(单位是“点”而非像素)。

print(screen.frame)  // 如:x: 0.0, y: 0.0, width: 1440.0, height: 900.0

原点为主屏幕的左下角。

多屏布局中,副屏可能是负坐标或 Y 向上扩展。

for screen in NSScreen.screens {
    print("屏幕尺寸\(screen.frame),屏幕ID:\(screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0)")
}

// 输出屏幕尺寸
屏幕尺寸(0.0, 0.0, 1470.0, 956.0),屏幕ID:1
屏幕尺寸(1470.0, -124.0, 1920.0, 1080.0),屏幕ID:2

4、visibleFrame: NSRect

屏幕去掉菜单栏和 Dock 后的可用区域。

print(screen.visibleFrame)

用途:自动布局 App 窗口时,避免被菜单栏遮挡。

5、backingScaleFactor: CGFloat

屏幕的缩放因子(Retina 屏通常为 2.0)

let scale = screen.backingScaleFactor

非 Retina 显示器:1.0,Retina 显示器:2.0。

需要换算成像素大小时使用:

let widthInPixels = screen.frame.width * screen.backingScaleFactor

6、colorSpace: NSColorSpace?

当前屏幕的颜色空间(sRGB、Display P3等)。

print(screen.colorSpace?.localizedName ?? "Unknown")

用途:做色彩敏感处理(图像处理、设计工具等)

7、deviceDescription: [NSDeviceDescriptionKey: Any]

包含屏幕底层硬件 ID 和其他信息。

最常用的是获取 CGDirectDisplayID:

if let screenID = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID {
    print("屏幕ID:\(screenID)")
}

可用于结合 CGDisplay 或 CoreGraphics API 来截图或标识屏幕。

8、localizedName: String (macOS 10.15+)

屏幕的名称,如“内建视网膜显示器”、“DELL U2723QE”等

print(screen.localizedName)

用途:多屏设置界面、用户选择某块屏幕时的名称显示。

常用方法

NSScreen 本身几乎没有方法,只有属性。

但配合它的属性和系统其他 API,可以实现很多高级功能,例如:

1、获取像素尺寸:frame.size × backingScaleFactor;

2、截图:获取 CGDirectDisplayID 然后用 CGDisplayCreateImage(…);

3、窗口自适应:visibleFrame + NSWindow.setFrame(…);

4、判断鼠标在哪块屏幕:NSEvent.mouseLocation + NSMouseInRect;

5、多屏定位:遍历 NSScreen.screens,找坐标对应区域。

使用示例

1、显示所有屏幕信息

for screen in NSScreen.screens {
    let name = screen.localizedName
    let frame = screen.frame
    let visible = screen.visibleFrame
    let scale = screen.backingScaleFactor
    let color = screen.colorSpace?.localizedName ?? "Unknown"
    
    print("""
    屏幕: \(name)
    尺寸: \(frame)(可用区域: \(visible))
    缩放因子: \(scale)
    色彩空间: \(color)
    """)
}

2、找出鼠标在哪个屏幕

func screenContainingMouse() -> NSScreen? {
    let mouseLocation = NSEvent.mouseLocation
    return NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) })
}

3、获取 CGDisplayID

for screen in NSScreen.screens {
    if let screenID = screen.deviceDescription[.init("NSScreenNumber")] as? CGDirectDisplayID {
        print("屏幕 ID: \(screenID), 尺寸: \(screen.frame.size)")
    }
}

4、获取主屏幕

let mainDisplayID = CGMainDisplayID()   // 主屏幕(主显示器)的唯一 ID

guard let screen = NSScreen.screens.first(where: {
    let screenID = $0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID
    return screenID == mainDisplayID
}) else {
    return
}

注意事项

AppKit的坐标系统以主屏幕的左下角为原点。

多个屏幕时,其 frame.origin 会不同,例如副屏可能在主屏上方或左侧。

可以通过 NSScreen.screens 遍历所有屏幕,定位鼠标在哪个屏幕中。

总结

NSScreen可以获取各屏幕的信息,配合实现多屏幕截图、窗口定位和Retina适配。

NSScreen只表示物理显示器,也就是当前连接的:

电脑自身的屏幕(如 MacBook 内建 Retina 显示器);

外接显示器(通过 HDMI、DisplayPort、USB-C 等);

通过 AirPlay 投影的显示器;

通过 Sidecar/iPad 扩展的屏幕。

NSScreen.screens.count // 返回物理屏幕个数

不包含macOS的虚拟桌面/控件(Spaces)。

通过三指滑动切换的 “全屏 App” 或 “Mission Control 添加的多个桌面”,在系统底层是同一个物理屏幕的不同虚拟工作区(Space),不是新的 NSScreen。

扩展知识

NSScreen和CGDirectDisplayID区别

在使用示例中,使用CGDirectDisplayID获取主屏幕:

let mainDisplayID = CGMainDisplayID()   // 主屏幕(主显示器)的唯一 ID

guard let screen = NSScreen.screens.first(where: {
    let screenID = $0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID
    return screenID == mainDisplayID
}) else {
    return
}

在所有 NSScreen 中,找出与主屏幕 CGDisplay 的 ID(mainDisplayID)一致的那一块 NSScreen。

CGMainDisplayID()是CoreGraphics 提供的函数:返回主屏幕(主显示器)的唯一 ID(CGDirectDisplayID 类型),这个 ID 是一个 UInt32,表示 macOS 系统层面的主屏幕编号。

 例如,我有两个屏幕:内建MacBook屏幕和外接屏幕(液晶电视)。

首次连接外接屏幕时,设置为“扩展显示屏”。

打开“设置”-“显示屏”:

可以看到这两个屏幕,点击“用途”按钮,可以切换为“主显示器”或“扩展显示器”。

上图把HDMI液晶电视设置为主显示器,内建MacBook屏幕就变为“扩展显示器”。

CGMainDisplayID()始终返回设置为“主显示器”的CGDirectDisplayID,无论窗口在哪一块屏幕上,永远指向系统设置里“主显示”的那一块屏幕。也就意味着,CGDirectDisplayID只会返回我们设置为主显示器的HDMI液晶电视。

NSScreen.main表示当前App激活窗口所在的屏幕,如果点击HDMI液晶电视屏幕上的应用程序,NSScreen.main就是HDMI液晶电视屏幕。如果点击内建MacBook屏幕的应用程序,NSScreen.main指的就是内建MacBook屏幕。

所以NSScreen.main表示当前活跃窗口的屏幕,不是设置中主显示器的屏幕。

这里也可能通过代码来验证:

// 遍历所有屏幕尺寸和屏幕ID
for screen in NSScreen.screens {
    print("屏幕尺寸\(screen.frame),屏幕ID:\(screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0)")
}

// 返回 NSScreen.main 的屏幕ID
if let main = NSScreen.main {
    print("NSScreen.main的屏幕ID:\(main.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0)")
}

// 返回 CGMainDisplayID() 的屏幕ID
guard let screen = NSScreen.screens.first(where: {
    let screenID = $0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID
    return screenID == CGMainDisplayID()
}) else {
    return
}
print("CGDirectDisplayID对应的主屏幕ID:\(screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0)")

在代码运行时,我把主屏幕设置为内建MacBook屏幕,HDMI液晶电视设置为扩展屏幕。

当我在内建MacBook屏幕,点击按钮触发这个方法,Xcode输出:

屏幕尺寸(0.0, 0.0, 1470.0, 956.0),屏幕ID:1
屏幕尺寸(1470.0, -124.0, 1920.0, 1080.0),屏幕ID:2
NSScreen.main的屏幕ID:1
CGDirectDisplayID对应的主屏幕ID:1

这表示内建MacBook屏幕是当前活跃窗口的屏幕,ID为1,也是设置中CGDirectDisplayID对应的主屏幕。

我切换到HDMI液晶电视,点击按钮触发这个方法,Xcode输出:

屏幕尺寸(0.0, 0.0, 1470.0, 956.0),屏幕ID:1
屏幕尺寸(1470.0, -124.0, 1920.0, 1080.0),屏幕ID:2
NSScreen.main的屏幕ID:2
CGDirectDisplayID对应的主屏幕ID:1

可以看到,因为我的鼠标移动到HDMI液晶电视,点击按钮触发方法,所以NSScreen.main也变成了HDMI液晶电视。而CGDirectDisplayID只会绑定设置中设置为主屏幕的屏幕。

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注