Как сделать снимок экрана окна specfi c в Qt (Python, Linux), даже если windows перекрываются? - PullRequest
0 голосов
/ 15 марта 2020

Я пытаюсь сделать скриншот текущего активного окна в PyQt5. Я знаю, что метод generi c для создания снимка экрана любого окна - QScreen::grabWindow(winID), для которого winID - это идентификатор реализации c ID в зависимости от оконной системы . Поскольку я использую X и KDE, я планирую в конечном итоге использовать CTypes для вызова Xlib, но сейчас я просто выполняю «xdotool getactivewindow», чтобы получить windowID в оболочке.

Для минимального примера я создал QMainWindow с QTimer. Когда таймер срабатывает, я идентифицирую идентификатор активного окна, выполняя «xdotool getactivewindow», получаю его возвращаемое значение, вызываю 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.

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

Однако снимок экрана не работает должным образом, если между окном приложения и активным окном происходит перекрытие. Окно самого приложения будет захвачено и создаст положительный отзыв.

Если между окном приложения и активным окном есть перекрытие. Окно самого приложения будет захвачено и создает положительный отзыв.

Я уже отключил 3D-композит в настройках KDE, но проблема остается. Приведенные выше примеры взяты с отключением всех составных эффектов.

Вопрос

  1. Почему эта реализация не работает правильно, когда окно приложения и активное окно перекрываются? Я подозреваю, что это проблема, вызванная некоторыми формами нежелательного взаимодействия между графическими системами (Qt toolkit, window manager, X, et c), но я не уверен.

  2. Можно ли вообще решить эту проблему? (Примечание: я знаю, что могу hide() перед снимком экрана и show() снова, но это не решает эту проблему, которая делает снимок экрана, даже если существует перекрытие.)

1 Ответ

0 голосов
/ 15 марта 2020

Как указывает @ eyllanes c, кажется, что это невозможно сделать в Qt, по крайней мере, не с QScreen::grabWindow, потому что grabWindow() на самом деле не захватывает само окно, а просто площадь, занимаемая окном. Документация содержит следующее предупреждение.

Функция grabWindow () захватывает пиксели с экрана, а не из окна, т. Е. Если над ним частично или полностью находится другое окно вы захватываете, вы также получаете пиксели из вышележащего окна. Курсор мыши, как правило, не захватывается.

Вывод состоит в том, что это невозможно сделать в чистом Qt. Реализовать такую ​​функциональность можно только написав низкоуровневую X-программу. Поскольку вопрос требует решения «в Qt», любой ответ, который потенциально предполагает более глубокие, низкоуровневые X-решения, выходит за рамки. Этот вопрос можно пометить как решенный.

Урок, который нужно выучить здесь: Всегда Проверьте документацию перед использованием функции или метода.


Обновление: мне удалось решить проблему, прочитав окно прямо из X через Xlib. По иронии судьбы, мое решение использует GTK, чтобы захватить окно и отправить его результат в Qt ... В любом случае, вы можете написать ту же самую программу для Xlib напрямую, если вы не хотите использовать GTK, но я использовал GTK, так как Xlib-связанный Функции в GDK довольно удобны для демонстрации концепции basi c.

Чтобы получить снимок экрана, мы сначала преобразуем наш идентификатор окна в GdkWindow, подходящий для использования в GDK, и мы вызываем Gdk.pixbuf_get_from_window(), чтобы захватить окно и сохранить его в gdk_pixbuf. Наконец, мы вызываем save_to_bufferv(), чтобы преобразовать необработанный pixbuf в подходящий формат изображения и сохранить его в буфере. На этом этапе изображение в буфере подходит для использования в любой программе, включая Qt.

. Документация содержит следующее предупреждение:

Если окно не отображается на экране, тогда нет данных изображения в затемненных / закадровых областях для размещения в буфере изображения. Содержимое частей pixbuf, соответствующих области вне экрана, не определено.

Если окно, из которого вы получаете данные, частично скрыто другими windows, то содержимое областей 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()

Теперь он отлично фиксирует активное окно, даже если поверх него перекрывается windows.

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

...