注意:这个问题有意非常一般(例如,请求Objective-C和Swift代码示例),因为它旨在记录如何尽可能可访问地捕获macOS上的窗口截图。
我想在Objective-C / Swift代码中捕获macOS窗口的屏幕截图。我知道这是可能的,因为有很多方法可以在macOS上截取屏幕截图(⇧⌘4,Grab实用程序,命令行上的screencapture
,...),但我不知道如何在我自己的代码中执行此操作。理想情况下,我可以指定特定应用程序的窗口,然后在NSImage
或CGImage
中捕获它,然后我可以处理并显示给用户或存储在文件中。
可以通过Quartz Window Services(一种Core Graphics框架的工具)在macOS上进行屏幕截图。我们的关键功能是CGWindowListCreateImage
,它“根据动态生成的窗口列表返回合成图像”,或者换句话说,根据指定的条件查找窗口,并创建包含每个窗口内容的图像。完善!其声明如下:
CGImageRef CGWindowListCreateImage(CGRect screenBounds,
CGWindowListOption listOption,
CGWindowID windowID,
CGWindowImageOption imageOption);
因此,为了捕获屏幕上的一个特定窗口,我们需要它的窗口ID(CGWindowID
)。为了检索它,我们首先需要一个系统上所有可用窗口的列表。我们通过CGWindowListCopyWindowInfo
得到它,它使用CGWindowListOption
s和相应的CGWindowID
,它们一起选择要包含在结果列表中的窗口。要获得所有窗口,我们分别指定kCGWindowListOptionAll
和kCGNullWindowID
。此外,如果您还没有弄明白,这是一个C API,所以我们将使用桥接转换来处理更友好的Objective-C容器而不是Core Foundation容器。
Objective-C的:
NSArray<NSDictionary*> *windowInfoList = (__bridge_transfer id)
CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
迅速:
let windowInfoList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID)!
as NSArray
从这里开始,我们需要将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;
迅速:
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的窗口:
Objective-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)
}
}
我们可以通过测试信息字典的其他键来完成上面的额外过滤 - 例如,通过名称(kCGWindowName
),或者窗口是否在屏幕上(kCGWindowIsOnscreen
) - 但是现在,我们只需要第一个窗口在列表中:
Objective-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
作为我们想要捕获的窗口传递。
第四,也是最后,我们可以用CGWindowImageOption
参数指定imageOption
s。这些会影响所得图像的外观;你可以通过按位OR组合它们。完整列表是here,但常见的包括kCGWindowImageDefault
,它捕获窗口的内容及其框架和阴影,或kCGWindowImageBoundsIgnoreFraming
,只捕获内容,和kCGWindowImageBestResolution
,以最佳分辨率捕获窗口的内容,无论实际大小(并且,取决于窗口,可能相当大),或kCGWindowImageNominalResolution
,它以屏幕上的当前大小捕获窗口。在这里,我和kCGWindowImageBoundsIgnoreFraming
和kCGWindowImageNominalResolution
一起只捕获与屏幕上相同大小的内容。
Aaand,鼓声请:
Objective-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])