如何截取 Qt 中特定 window(Python、Linux)的屏幕截图,即使 windows 重叠?

How do I take a screenshot of a specfic window in Qt (Python, Linux), even if the windows are overlapped?

我正在尝试截取 PyQt5 中当前活动 window 的屏幕截图。我知道截取任何 window 的通用方法是 QScreen::grabWindow(winID),其中 winIDimplementation-specific ID depending on the window system。由于我是 运行 X 和 KDE,我打算最终使用 CTypes 来调用 Xlib,但是现在,我只是执行 "xdotool getactivewindow" 来获取 shell 中的 windowID ].

举个最简单的例子,我创建了一个带有 QTimer 的 QMainWindow。当计时器触发时,我通过执行 "xdotool getactivewindow" 识别活动 window ID,获取其 return 值,调用 grabWindow() 捕获活动 window,并显示QLabel 中的截图。在启动时,我还将我的 window 设置为固定的 500x500 大小以供观察,并激活 Qt.WindowStaysOnTopHint 标志,以便我的 window 在未聚焦时仍然可见。把它们放在一起,实现就是下面的代码。

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()

为了测试实现,我启动了脚本并单击了另一个 window。如果我的应用程序 window 和活动 window 之间没有重叠,它似乎可以正常工作。 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.

但是,如果应用程序 window 和活动 window 之间存在重叠,则屏幕截图无法按预期工作。应用程序本身的 window 将被捕获,并产生积极的反馈。

如果应用程序 window 和活动 window 之间存在重叠。应用程序本身的 window 将被捕获,并产生积极的反馈。

我已经在 KDE 的设置中禁用了 3D 合成,但问题仍然存在。上面的示例是在禁用所有复合效果的情况下进行的。

问题

  1. 为什么当应用程序 window 和活动 window 重叠时此实现无法正常工作?我怀疑这是由图形系统(Qt 工具包、window 管理器、X 等)之间某些形式的不需要的交互引起的问题,但我不确定。

  2. 有没有可能解决这个问题? (注意:我知道我可以在截图前hide(),然后再show(),但它并没有真正解决这个问题,即使存在重叠也正在截图。)

正如@eyllanesc 所指出的,似乎在 Qt 中不可能做到这一点,至少在 QScreen::grabWindow 中是不可能的,因为 grabWindow() 实际上并没有抓住 window本身,而仅仅是window占用的区域。 The documentation 包含以下警告。

The grabWindow() function grabs pixels from the screen, not from the window, i.e. if there is another window partially or entirely over the one you grab, you get pixels from the overlying window, too. The mouse cursor is generally not grabbed.

结论是在纯 Qt 中不可能做到这一点。只能通过编写低级 X 程序来实现这样的功能。由于问题要求“在 Qt 中”提供解决方案,因此任何可能涉及更深层次、低级别 X 解决方案的答案都超出了范围。这个问题可以标记为已解决。

这里要吸取的教训:总是在使用函数或方法之前检查文档。


更新:我设法通过 Xlib 直接从 X 读取 window 来解决问题。有点讽刺的是,我的解决方案使用 GTK 获取 window 并将其结果发送到 Qt...无论如何,如果你不想使用 GTK,你可以直接用 Xlib 编写相同的程序,但我使用 GTK 因为GDK中的Xlib相关函数非常方便演示基本概念。

为了获取屏幕截图,我们首先将我们的 window ID 转换为适合在 GDK 中使用的 GdkWindow,然后我们调用 Gdk.pixbuf_get_from_window() 来获取 window 和将其存储在 gdk_pixbuf 中。最后,我们调用 save_to_bufferv() 将原始 pixbuf 转换为合适的图像格式并将其存储在缓冲区中。至此,缓冲区中的图像适合在任何程序中使用,包括Qt。

文档包含以下警告:

If the window is off the screen, then there is no image data in the obscured/offscreen regions to be placed in the pixbuf. The contents of portions of the pixbuf corresponding to the offscreen region are undefined.

If the window you’re obtaining data from is partially obscured by other windows, then the contents of the pixbuf areas corresponding to the obscured regions are undefined.

If the window is not mapped (typically because it’s iconified/minimized or not on the current workspace), then NULL will be returned.

If memory can’t be allocated for the return value, NULL will be returned instead.

它还有一些关于合成的评论,

gdk_display_supports_composite has been deprecated since version 3.16 and should not be used in newly-written code.

Compositing is an outdated technology that only ever worked on X11.

所以基本上,只能在 X11 下使用合成 window 管理器抓取部分模糊的 window(在 Wayland 中不可能!)。我在没有合成的情况下对其进行了测试,发现禁用合成时 window 被涂黑了。但是当启用组合时,它似乎没有问题。它可能适用于您的应用程序,也可能不适用于您的应用程序。但我认为如果您在 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()

现在它可以完美地捕获活动的 window,即使上面有重叠的 windows。