python - 即使窗口重叠,如何在 Qt(Python、Linux)中截取特定窗口的屏幕截图?
问题描述
我正在尝试截取 PyQt5 中当前活动窗口的屏幕截图。我知道对任何窗口进行屏幕截图的通用方法是QScreen::grabWindow(winID)
,它winID
是一个特定于实现的 ID,具体取决于窗口系统。由于我正在运行 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()
为了测试实现,我启动了脚本并单击了另一个窗口。如果我的应用程序窗口和活动窗口之间没有重叠,它似乎可以正常工作。See the following screenshot, when Firefox (right) is selected, my application is able to capture the active window of Firefox and display it in the QLabel.
但是,如果应用程序窗口和活动窗口之间存在重叠,则屏幕截图不会按预期工作。应用程序本身的窗口将被捕获,并创建一个积极的反馈。
如果应用程序窗口和活动窗口之间存在重叠。应用程序本身的窗口将被捕获,并创建一个积极的反馈。
我已经在 KDE 的设置中禁用了 3D 合成,但问题仍然存在。上面的示例是在禁用所有复合效果的情况下拍摄的。
问题
当应用程序窗口和活动窗口重叠时,为什么这个实现不能正常工作?我怀疑这是由图形系统(Qt 工具包、窗口管理器、X 等)之间某些形式的不需要的交互引起的问题,但我不确定。
甚至有可能解决这个问题吗?(注意:我知道我可以
hide()
在截图之前再次截图show()
,但这并不能真正解决这个问题,即使存在重叠也会截图。)
解决方案
正如@eyllanesc 所指出的,在Qt 中似乎不可能做到这一点,至少不能使用QScreen::grabWindow
,因为grabWindow()
实际上并没有抓住窗口本身,而只是抓住了窗口所占据的区域。该文档包含以下警告。
grabWindow() 函数从屏幕而不是窗口中抓取像素,也就是说,如果在您抓取的窗口上部分或全部有另一个窗口,您也会从上层窗口中获取像素。鼠标光标一般不会被抓取。
结论是在纯 Qt 中不可能做到这一点。只有编写低级 X 程序才能实现这样的功能。由于该问题要求“在 Qt 中”提供解决方案,因此任何可能涉及更深层次的低级 X 解决方案的答案都超出了范围。这个问题可以标记为已解决。
这里要学习的教训:在使用函数或方法之前,请务必检查文档。
更新:我设法通过 Xlib 直接从 X 读取窗口来解决问题。有点讽刺的是,我的解决方案使用 GTK 抓取窗口并将其结果发送到 Qt...无论如何,如果您不想使用 GTK,您可以直接使用 Xlib 编写相同的程序,但我使用 GTK 因为 Xlib 相关GDK 中的函数非常方便地演示基本概念。
要获取屏幕截图,我们首先将窗口 ID 转换为GdkWindow
适合在 GDK 中使用的 ID,然后调用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()
现在它完美地捕获了一个活动窗口,即使它上面有重叠的窗口。
推荐阅读
- html - 如果子div没有价值,如何隐藏父div
- python - 如何使用 Anaconda 3 (Spyder) 启动 Pygubu?
- javascript - 导航栏不再滚动到页面上的部分?
- vcl - 在 Delphi 10.3 中编译 KBMMW 设计包:vcl 冲突错误 (E2199)
- reactjs - 尝试从 reducer 返回数组时应用程序崩溃
- php - Laravel:WhereBetween Date 与请求日期
- c++ - QT 和 OpenCV 问题在不同线程中的 QGraphicsView 上查看 Opencv Mat :: QObject::killTimer: 计时器不能从另一个线程停止
- vba - SAP OLE 自动化 Logon() 方法参数
- django - Django - 根据加载的视图选择选择值
- sql - 是否存在允许列和值之间视觉对称的 INSERT 语法?