如何在Qt(Python、Linux)中截取特定窗口的屏幕快照,即使窗口重叠?

2024-09-27 09:32:38 发布

您现在位置:Python中文网/ 问答频道 /正文

我正在尝试拍摄PyQt5中当前活动窗口的屏幕截图。我知道拍摄任何窗口截图的通用方法是QScreen::grabWindow(winID),其中winIDimplementation-specific ID depending on the window system。因为我运行的是X和KDE,所以我计划最终使用CTypes调用Xlib,但现在,我只需执行“xdotool getactivewindow”来获取shell中的windowID

对于最小的exmaple,我创建了一个带有QTimer的QMainWindow。当启动计时器时,我通过执行“xdool getactivewindow”来识别活动窗口ID,获取其返回值,调用grabWindow()来捕获活动窗口,并在QLabel中显示screetshot。在启动时,我还将我的窗口设置为固定的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中

When Firefox (right) is selected, my application is able to capture the active window of Firefox and display it in the QLabel.

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

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

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

问题:

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

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


Tags: self应用程序屏幕windowqtlabelpyqt5timer
1条回答
网友
1楼 · 发布于 2024-09-27 09:32:38

正如@eyllanesc所指出的,似乎不可能在Qt中实现这一点,至少不可能在QScreen::grabWindow中实现,因为grabWindow()实际上并没有捕获窗口本身,而只是捕获窗口所占据的区域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读取窗口,成功地解决了这个问题。有点讽刺的是,我的解决方案使用GTK抓取窗口并将其结果发送给Qt。。。无论如何,如果不想使用GTK,可以直接用Xlib编写相同的程序,但我使用了GTK,因为GDK中与Xlib相关的函数非常方便地演示了基本概念

为了获得屏幕截图,我们首先将窗口ID转换为适合在GDK中使用的GdkWindow,然后调用Gdk.pixbuf_get_from_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下抓取部分遮挡的窗口(在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()

现在它完美地捕获了一个活动窗口,即使上面有重叠的窗口

Now it captures an active window perfectly, even if there are overlapping windows on top of it.

相关问题 更多 >

    热门问题