macOS上的屏幕截图可以通过
Quartz Window Services实现,这是Core Graphics框架的一个功能。我们在这里的关键函数是
CGWindowListCreateImage
,它“基于动态生成的窗口列表返回一个组合图像”,换句话说,根据指定的条件查找窗口并创建包含每个窗口内容的图像。太完美了!其声明如下:
CGImageRef CGWindowListCreateImage(CGRect screenBounds,
CGWindowListOption listOption,
CGWindowID windowID,
CGWindowImageOption imageOption);
因此,为了捕获屏幕上的一个特定窗口,我们需要它的窗口ID(
CGWindowID
)。为了检索它,我们首先需要获取系统上所有可用窗口的列表。我们通过
CGWindowListCopyWindowInfo
获取该列表,它需要
CGWindowListOption
和相应的
CGWindowID
,两者共同选择要包含在结果列表中的窗口。要获取
所有窗口,我们分别指定
kCGWindowListOptionAll
和
kCGNullWindowID
。此外,如果您还没有弄清楚,这是一个C API,因此我们将使用桥接转换来使用更友好的Objective-C容器而不是Core Foundation容器。
Objective-C:
NSArray<NSDictionary*> *windowInfoList = (__bridge_transfer id)
CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
Swift:
let windowInfoList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID)!
as NSArray
从这里开始,我们需要将我们的windowInfoList
过滤到我们想要的特定窗口。很有可能我们首先想要按应用程序进行过滤。为此,我们需要我们选择的应用程序的进程ID。我们可以使用NSRunningApplication
来实现:
Objective-C:
NSArray<NSRunningApplication*> *apps =
[NSRunningApplication runningApplicationsWithBundleIdentifier:
@"com.apple.Safari"];
if (apps.count == 0) {
puts("The application is not running");
return;
}
pid_t appPID = apps[0].processIdentifier;
Swift:
let apps = NSRunningApplication.runningApplications(withBundleIdentifier:
"com.apple.Safari")
if apps.isEmpty {
print("The application is not running")
return
}
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];
}
}
Swift:
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))来进行额外的过滤,但现在,我们将只获取列表中的第一个窗口:
Objective-C:
NSDictionary *appWindowInfo = appWindowsInfoList[0];
CGWindowID windowID = [appWindowInfo[(__bridge NSString *)kCGWindowNumber] unsignedIntValue];
Swift:
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
来进行捕获。根据
文档,我们可以将此参数指定为
CGRectNull
,以尽可能紧密地包含所有指定的窗口。 对我很有用。
其次,我们必须指定如何使用
listOption
选择我们的窗口。 我们实际上之前在调用
CGWindowListCopyWindowInfo
时使用了其中之一,但是那里我们想要系统中的所有窗口; 在这里,我们只想要一个,因此我们将指定
kCGWindowListOptionIncludingWindow
,与
其文档页面相反,这对于
CGWindowListCreateImage
本身是有意义的,因为它指定了我们传递的窗口,仅传递该窗口。
第三,我们将我们的
windowID
作为我们要捕获的窗口传递。
第四,也是最后一点,我们可以使用
imageOption
参数指定
CGWindowImageOption
。这些选项会影响所得到的图像外观;你可以通过按位 OR 结合它们。完整列表
在此, 但常见的包括
kCGWindowImageDefault
,它捕获窗口内容及其框架和阴影或
kCGWindowImageBoundsIgnoreFraming
,它只捕获内容,
kCGWindowImageBestResolution
捕获最佳分辨率可用的窗口内容,而不考虑实际大小(并且可能相当大),或者
kCGWindowImageNominalResolution
,它以屏幕上的当前大小捕获窗口。在这里,我选择了
kCGWindowImageBoundsIgnoreFraming
和
kCGWindowImageNominalResolution
来捕获与屏幕上相同大小的内容。
鼓声响起:
Objective-C:
CGImageRef windowImage =
CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow,
windowID, kCGWindowImageBoundsIgnoreFraming|
kCGWindowImageNominalResolution);
// NOTE: windowImage may be NULL if the capture failed
Swift:
let windowImage: CGImage? =
CGWindowListCreateImage(.null, .optionIncludingWindow, windowID,
[.boundsIgnoreFraming, .nominalResolution])
viewDidAppear
方法中获取窗口ID?我尝试了view.window?.windowNumber
,但它不是由窗口服务器分配的全局窗口号。 - Kevin YueCGWindowListCopyWindowInfo
查找窗口 ID 的;你试过那些指示了吗? - ThatsJustCheesyCGWindowListCopyWindowInfo
来找到窗口信息列表,然后通过从ProcessInfo().processIdentifier
获取的当前进程ID过滤列表,解决了我的问题。 - Kevin Yue