我正在尝试截取PyQt5中当前活动窗口的屏幕截图。我知道拍摄任何窗口的屏幕快照的通用方法是QScreen::grabWindow(winID)
,对此,winID
是implementation-specific ID depending on the window system。由于我正在运行X和KDE,因此我打算最终使用CTypes来调用Xlib,但是现在,我只执行“ xdotool getactivewindow”以在shell中获取windowID。
为了获得最小的实例,我创建了带有QTimer的QMainWindow。当计时器启动时,我通过执行“ xdotool getactivewindow”来标识活动窗口ID,获取其返回值,调用grabWindow()捕获活动窗口,并在QLabel中显示屏幕快照。在启动时,我还将窗口设置为固定的500x500大小以进行观察,并激活Qt.WindowStaysOnTopHint
标志,以便在窗口未聚焦时仍可见。将它们放在一起,实现是以下代码。
from PyQt5 import QtCore, QtGui, QtWidgets
import subprocess
class ScreenCapture(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.setFixedHeight(500)
self.setFixedWidth(500)
self.label = QtWidgets.QLabel(self)
self.timer = QtCore.QTimer(self)
self.timer.setInterval(500)
self.timer.timeout.connect(self.timer_handler)
self.timer.start()
self.screen = QtWidgets.QApplication.primaryScreen()
@QtCore.pyqtSlot()
def timer_handler(self):
window = int(subprocess.check_output(["xdotool", "getactivewindow"]).decode("ascii"))
self.screenshot = self.screen.grabWindow(window)
self.label.setPixmap(self.screenshot)
self.label.setFixedSize(self.screenshot.size())
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = ScreenCapture()
window.show()
app.exec()
为了测试实现,我启动了脚本并单击了另一个窗口。如果我的应用程序窗口和活动窗口之间没有重叠,它似乎可以正常工作。请参见以下屏幕截图,选择Firefox(右)后,我的应用程序便能够捕获Firefox的活动窗口并将其显示在QLabel中。
但是,如果应用程序窗口和活动窗口之间存在重叠,则屏幕截图将无法正常工作。应用程序本身的窗口将被捕获,并创建一个积极的反馈。
如果应用程序窗口和活动窗口之间存在重叠。应用程序本身的窗口将被捕获,并创建一个积极的反馈。
我已经在KDE的设置中禁用了3D合成,但是问题仍然存在。上面的示例在禁用所有复合效果的情况下进行。
当应用程序窗口和活动窗口重叠时,为什么此实现无法正常工作?我怀疑这是图形系统之间某些形式的不必要交互(Qt工具包,窗口管理器,X等)引起的问题,但我不确定。
是否有可能解决这个问题? (注意:我知道我可以在屏幕截图之前单击hide()
,然后再次单击show()
,但是它并不能真正解决此问题,即使存在重叠也需要截图)。
正如@eyllanesc指出的那样,似乎不可能在Qt中执行此操作,至少不能使用QScreen::grabWindow
执行此操作,因为grabWindow()
实际上并没有抓住窗口本身,而只是抓住了窗口占用的区域。窗口。 The documentation包含以下警告。
[grabWindow()函数从屏幕上而不是从窗口中捕获像素,即,如果您抓取的那个窗口中有部分或全部是另一个窗口,那么您也将从覆盖的窗口中获取像素。通常不会抓住鼠标光标。
结论是,在纯Qt中不可能做到这一点。只能通过编写低级X程序来实现这种功能。由于该问题要求使用“ Qt”解决方案,因此任何可能涉及更深层次的低级X解决方案的答案都不在范围之内。该问题可以标记为已解决。
此处要学习的课程:始终在使用函数或方法之前先检查文档。
更新:我设法通过Xlib从X直接读取窗口来解决此问题。具有讽刺意味的是,我的解决方案使用GTK来捕获窗口并将其结果发送给Qt ...无论如何,如果您不想使用GTK,则可以直接用Xlib编写相同的程序,但是我使用GTK是因为与Xlib相关GDK中的函数非常方便地演示了基本概念。
[要获取屏幕截图,我们首先将窗口ID转换为适合在GDK中使用的GdkWindow
,然后调用Gdk.pixbuf_get_from_window()
抓取窗口并将其存储在gdk_pixbuf
中。最后,我们调用save_to_bufferv()
将原始pixbuf转换为合适的图像格式并将其存储在缓冲区中。此时,缓冲区中的图像适合在任何程序中使用,包括Qt。
文档包含以下警告:
如果窗口不在屏幕上,则在遮盖/屏幕外区域中没有要放置在pixbuf中的图像数据。 pixbuf中与屏幕外区域相对应的部分的内容未定义。
如果您要从中获取数据的窗口被其他窗口部分遮盖,则对应于遮盖区域的pixbuf区域的内容未定义。
如果窗口未映射(通常是因为图标已被缩小/缩小或不在当前工作空间中,则将返回NULL。
如果无法为返回值分配内存,则将返回NULL。
也有一些关于合成的评论,
[
gdk_display_supports_composite
从版本3.16开始不推荐使用,不应在新编写的代码中使用。合成是一种过时的技术,只能在X11上使用。
因此,基本上,只能使用合成窗口管理器在X11下抓取部分遮盖的窗口(在Wayland中是不可能的!)。我在没有合成的情况下对其进行了测试,发现禁用合成时该窗口变黑了。但是启用合成后,它似乎可以正常工作。它可能适用于您的应用程序,也可能无法适用。但是我认为,如果您在X11下使用合成,它可能会起作用。
from PyQt5 import QtCore, QtGui, QtWidgets
import subprocess
class ScreenCapture(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.setFixedHeight(500)
self.setFixedWidth(500)
self.label = QtWidgets.QLabel(self)
self.screen = QtWidgets.QApplication.primaryScreen()
self.timer = QtCore.QTimer(self)
self.timer.setInterval(500)
self.timer.timeout.connect(self.timer_handler)
self.timer.start()
@staticmethod
def grab_screenshot():
from gi.repository import Gdk, GdkX11
window_id = int(subprocess.check_output(["xdotool", "getactivewindow"]).decode("ascii"))
display = GdkX11.X11Display.get_default()
window = GdkX11.X11Window.foreign_new_for_display(display, window_id)
x, y, width, height = window.get_geometry()
pb = Gdk.pixbuf_get_from_window(window, 0, 0, width, height)
if pb:
buf = pb.save_to_bufferv("bmp", (), ())
return buf[1]
else:
return
@QtCore.pyqtSlot()
def timer_handler(self):
screenshot = self.grab_screenshot()
self.pixmap = QtGui.QPixmap()
if not self.pixmap:
return
self.pixmap.loadFromData(screenshot)
self.label.setPixmap(self.pixmap)
self.label.setFixedSize(self.pixmap.size())
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = ScreenCapture()
window.show()
app.exec()
现在,即使顶部有重叠的窗口,它也可以完美捕获活动窗口。