可以通过Quartz Window Services(Core Graphics 框架的一个工具)在 macOS 上进行屏幕捕获。我们这里的关键函数是CGWindowListCreateImage,它“根据动态生成的窗口列表返回合成图像”,或者换句话说,根据指定标准查找窗口并创建包含每个窗口内容的图像。完美的!其声明如下:
CGImageRef CGWindowListCreateImage(CGRect screenBounds,
CGWindowListOption listOption,
CGWindowID windowID,
CGWindowImageOption imageOption);
因此,为了捕捉屏幕上的一个特定窗口,我们需要它的窗口 ID (CGWindowID)。要检索它,我们首先需要系统上所有可用窗口的列表。我们通过CGWindowListCopyWindowInfo 得到它,它采用CGWindowListOptions 和相应的CGWindowID,它们一起选择要包含在结果列表中的窗口。要获得所有窗口,我们分别指定kCGWindowListOptionAll 和kCGNullWindowID。此外,如果您还没有弄清楚,这是一个 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,它以屏幕上的当前大小捕获窗口。在这里,我使用kCGWindowImageBoundsIgnoreFraming 和kCGWindowImageNominalResolution 只捕获与屏幕上相同大小的内容。
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])