【问题标题】:Capture screenshot of macOS window捕获 macOS 窗口的屏幕截图
【发布时间】:2017-12-30 01:47:58
【问题描述】:

注意:这个问题故意非常笼统(例如,要求提供 Objective-C 和 Swift 代码示例),因为它旨在记录如何在 macOS 上尽可能容易地捕获窗口屏幕截图。

我想用 Objective-C/Swift 代码截取一个 macOS 窗口的屏幕截图。我知道这是可能的,因为有多种方法可以在 macOS 上截屏(⇧⌘4,Grab 实用程序,命令行上的screencapture,......),但我不知道如何自己做代码。理想情况下,我能够指定特定应用程序的窗口,然后将其捕获到 NSImageCGImage 中,然后我可以处理并显示给用户或存储在文件中。

【问题讨论】:

    标签: objective-c swift macos screenshot


    【解决方案1】:

    可以通过Quartz Window Services(Core Graphics 框架的一个工具)在 macOS 上进行屏幕捕获。我们这里的关键函数是CGWindowListCreateImage,它“根据动态生成的窗口列表返回合成图像”,或者换句话说,根据指定标准查找窗口并创建包含每个窗口内容的图像。完美的!其声明如下:

    CGImageRef CGWindowListCreateImage(CGRect screenBounds, 
                                       CGWindowListOption listOption, 
                                       CGWindowID windowID, 
                                       CGWindowImageOption imageOption);
    

    因此,为了捕捉屏幕上的一个特定窗口,我们需要它的窗口 ID (CGWindowID)。要检索它,我们首先需要系统上所有可用窗口的列表。我们通过CGWindowListCopyWindowInfo 得到它,它采用CGWindowListOptions 和相应的CGWindowID,它们一起选择要包含在结果列表中的窗口。要获得所有窗口,我们分别指定kCGWindowListOptionAllkCGNullWindowID。此外,如果您还没有弄清楚,这是一个 C API,因此我们将使用桥接转换来处理更友好的 Objective-C 容器而不是 Core Foundation 容器。

    目标-C:

    NSArray<NSDictionary*> *windowInfoList = (__bridge_transfer id)
        CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
    

    斯威夫特:

    let windowInfoList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID)!
        as NSArray
    

    从这里开始,我们需要将windowInfoList 过滤到我们想要的特定窗口。我们可能希望首先按应用程序进行过滤。为此,我们需要我们选择的应用程序的进程 ID。我们可以使用NSRunningApplication 来实现:

    目标-C:

    NSArray<NSRunningApplication*> *apps = 
        [NSRunningApplication runningApplicationsWithBundleIdentifier:
            /* Bundle ID of the application, e.g.: */ @"com.apple.Safari"];
    if (apps.count == 0) {
        // Application is not currently running
        puts("The application is not running");
        return; // Or whatever
    }
    pid_t appPID = apps[0].processIdentifier;
    

    斯威夫特:

    let apps = NSRunningApplication.runningApplications(withBundleIdentifier:
        /* Bundle ID of the application, e.g.: */ "com.apple.Safari")
    if apps.isEmpty {
        // Application is not currently running
        print("The application is not running")
        return // Or whatever
    }
    let appPID = apps[0].processIdentifier
    

    有了appPID,我们现在可以继续过滤我们的窗口信息列表,只筛选出拥有匹配所有者 PID 的窗口:

    目标-C:

    NSMutableArray<NSDictionary*> *appWindowsInfoList = [NSMutableArray new];
    for (NSDictionary *info in windowInfoList) {
        if ([info[(__bridge NSString *)kCGWindowOwnerPID] integerValue] == appPID) {
            [appWindowsInfoList addObject:info];
        }
    }
    

    斯威夫特:

    var appWindowsInfoList = [NSDictionary]()
    for info_ in windowInfoList {
        let info = info_ as! NSDictionary
        if (info[kCGWindowOwnerPID as NSString] as! NSNumber).intValue == appPID {
            appWindowsInfoList.append(info)
        }
    }
    

    我们可以通过测试 info 字典的其他键来完成上述额外的过滤——例如,按名称 (kCGWindowName),或者按窗口是否在屏幕上 (kCGWindowIsOnscreen)——但现在,我们'将只取列表中的第一个窗口:

    目标-C:

    NSDictionary *appWindowInfo = appWindowsInfoList[0];
    CGWindowID windowID = [appWindowInfo[(__bridge NSString *)kCGWindowNumber] unsignedIntValue];
    

    斯威夫特

    let appWindowInfo: NSDictionary = appWindowsInfoList[0];
    let windowID: CGWindowID = (appWindowInfo[kCGWindowNumber as NSString] as! NSNumber).uint32Value
    

    我们有我们的窗口 ID!现在,我们再次调用该调用还需要什么?

    CGImageRef CGWindowListCreateImage(CGRect screenBounds, 
                                       CGWindowListOption listOption, 
                                       CGWindowID windowID, 
                                       CGWindowImageOption imageOption);
    

    首先,我们需要一个screenBounds 来捕获。根据the documentation,我们可以为此参数指定CGRectNull,以尽可能紧密地包围所有指定的窗口。对我有用。

    其次,我们必须使用listOption 指定我们希望如何选择我们的窗口。实际上,我们之前在调用CGWindowListCopyWindowInfo 时使用了其中之一,但是我们想要系统上的所有窗口;在这里,我们只想要一个,所以我们将指定kCGWindowListOptionIncludingWindow,与its documentation page 不同,CGWindowListCreateImage 本身有意义,因为它指定了我们通过的窗口,并且我们经过的窗口。

    第三,我们将windowID 作为我们想要捕获的窗口。

    第四也是最后,我们可以使用imageOption 参数指定CGWindowImageOptions。这些会影响生成图像的外观;您可以通过按位或组合它们。完整列表是here,但常见的包括kCGWindowImageDefault,它捕获窗口的内容以及它的框架和阴影,或kCGWindowImageBoundsIgnoreFraming,它只捕获内容,以及kCGWindowImageBestResolution,它捕获窗口的内容以可用的最佳分辨率,无论实际大小如何(并且,取决于窗口,可能相当大),或kCGWindowImageNominalResolution,它以屏幕上的当前大小捕获窗口。在这里,我使用kCGWindowImageBoundsIgnoreFramingkCGWindowImageNominalResolution 只捕获与屏幕上相同大小的内容。

    Aaand,请打鼓:

    目标-C:

    CGImageRef windowImage =
        CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow,
                                windowID, kCGWindowImageBoundsIgnoreFraming|
                                kCGWindowImageNominalResolution);
    // NOTE: windowImage may be NULL if the capture failed
    

    斯威夫特:

    let windowImage: CGImage? =
        CGWindowListCreateImage(.null, .optionIncludingWindow, windowID,
                                [.boundsIgnoreFraming, .nominalResolution])
    

    【讨论】:

    • 这是一个非常有用的答案,非常感谢。但是如何从 ViewController 中的viewDidAppear 方法中获取窗口 ID 呢?我试过view.window?.windowNumber,但不是窗口服务器分配的全局窗口号。
    • @Kevin 很大一部分答案专门用于使用CGWindowListCopyWindowInfo 查找窗口ID;您尝试过这些说明吗?
    • 我通过使用CGWindowListCopyWindowInfo 找到窗口信息列表并通过从ProcessInfo().processIdentifier 获得的当前进程ID 过滤列表来解决我的问题。
    • 在我的 Objective-C 程序中,我似乎得到了一个有效的(非 0)窗口 ID 值,但试图捕捉窗口图像却一无所获。我可以通过将kCGWindowListOptionOnScreenOnly 替换为kCGWindowListOptionAll 来解决此问题。
    • 一个很好的答案,但如果窗口部分在屏幕之外,那么只会捕获它的可见部分。而屏幕捕获实用程序可以对部分超出屏幕的窗口进行屏幕截图。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-22
    • 1970-01-01
    • 2012-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多