捕获macOS窗口的截图

Tha*_*esy 4 macos screenshot objective-c swift

注意:这个问题是故意很一般(比如他们的Objective-C和SWIFT代码示例要求),因为它是用来记录如何捕获窗口的截图在MacOS尽量通俗易懂越好.

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

Tha*_*esy 16

通过Quartz Window Services(Core Graphics框架的一个工具)可以在macOS上进行屏幕捕获.我们的主要功能就是在这里CGWindowListCreateImage,该"返回一个基于窗口的动态生成的列表上的合成图像",或者,换句话说,发现根据指定的标准窗口和创建具有每个内容的图像.完善!其声明如下:

CGImageRef CGWindowListCreateImage(CGRect screenBounds, 
                                   CGWindowListOption listOption, 
                                   CGWindowID windowID, 
                                   CGWindowImageOption imageOption);
Run Code Online (Sandbox Code Playgroud)

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

Objective-C的:

NSArray<NSDictionary*> *windowInfoList = (__bridge_transfer id)
    CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
Run Code Online (Sandbox Code Playgroud)

迅速:

let windowInfoList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID)!
    as NSArray
Run Code Online (Sandbox Code Playgroud)

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

Objective-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;
Run Code Online (Sandbox Code Playgroud)

迅速:

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
Run Code Online (Sandbox Code Playgroud)

随着appPID在手,我们现在可以继续前进,我们的窗口信息列表进行过滤,下降到仅具有匹配所有者PID窗口:

Objective-C的:

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

迅速:

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

我们可以通过测试信息字典的其他键来完成上面的额外过滤 - 例如,通过名称(kCGWindowName),或者窗口是否在屏幕上(kCGWindowIsOnscreen) - 但是现在,我们只需要在第一个窗口中列表:

Objective-C的:

NSDictionary *appWindowInfo = appWindowsInfoList[0];
CGWindowID windowID = [appWindowInfo[(__bridge NSString *)kCGWindowNumber] unsignedIntValue];
Run Code Online (Sandbox Code Playgroud)

斯威夫特:

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

我们有窗口ID!现在,我们还有什么需要再次接听电话?

CGImageRef CGWindowListCreateImage(CGRect screenBounds, 
                                   CGWindowListOption listOption, 
                                   CGWindowID windowID, 
                                   CGWindowImageOption imageOption);
Run Code Online (Sandbox Code Playgroud)

首先,我们需要一个screenBounds捕获.根据文档,我们可以指定CGRectNull此参数尽可能紧密地包含所有指定的窗口.适合我.

其次,我们必须指定我们如何选择我们的窗口listOption.在我们的调用中CGWindowListCopyWindowInfo,我们实际上使用过其中一个,但是我们想要系统上的所有窗口; 在这里,我们只需要一个,所以我们将指定kCGWindowListOptionIncludingWindow,与其文档页面相反,它本身有意义的CGWindowListCreateImage,因为它指定了我们传递的窗口,并且指定了我们传递的窗口.

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

第四,也是最后,我们可以CGWindowImageOptionimageOption参数指定s .这些会影响所得图像的外观; 你可以通过按位OR组合它们.完整列表在这里,但常见的包括kCGWindowImageDefault,它捕获窗口的内容及其框架和阴影,或者kCGWindowImageBoundsIgnoreFraming仅捕获内容,并且kCGWindowImageBestResolution以可用的最佳分辨率捕获窗口的内容,而不管实际大小(并且,取决于窗口,可能相当大),或者kCGWindowImageNominalResolution,它以屏幕上的当前大小捕获窗口.在这里,我只使用kCGWindowImageBoundsIgnoreFramingkCGWindowImageNominalResolution捕获与屏幕上相同大小的内容.

Aaand,鼓声请:

Objective-C的:

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

迅速:

let windowImage: CGImage? =
    CGWindowListCreateImage(.null, .optionIncludingWindow, windowID,
                            [.boundsIgnoreFraming, .nominalResolution])
Run Code Online (Sandbox Code Playgroud)