1

我正在使用 .将另一个窗口嵌入到 Qt 小部件中PySide2.QtGui.QWindow.fromWinId(windowId)。它运行良好,但是当原始 X11 窗口破坏它时它不会触发事件。

如果我运行下面的文件mousepad & python3 embed.py并按Ctrl+ Q,则不会触发任何事件,并且我会留下一个空的小部件。

如何检测由其导入的 X11 窗口何时QWindow.fromWinId被其创建者破坏?

现有 Mousepad 窗口、嵌入 embed.py 框架中的 mousepad 窗口和空 embed.py 框架的屏幕截图

#!/usr/bin/env python

# sudo apt install python3-pip
# pip3 install PySide2

import sys, subprocess, PySide2
from PySide2 import QtGui, QtWidgets, QtCore

class MyApp(QtCore.QObject):
  def __init__(self):
    super(MyApp, self).__init__()

    # Get some external window's windowID
    print("Click on a window to embed it")
    windowIdStr = subprocess.check_output(['sh', '-c', """xwininfo -int | sed -ne 's/^.*Window id: \\([0-9]\\+\\).*$/\\1/p'"""]).decode('utf-8')
    windowId = int(windowIdStr)
    print("Embedding window with windowId=" + repr(windowId))

    # Create a simple window frame
    self.app = QtWidgets.QApplication(sys.argv)
    self.mainWindow = QtWidgets.QMainWindow()
    self.mainWindow.show()

    # Grab the external window and put it inside our window frame
    self.externalWindow = QtGui.QWindow.fromWinId(windowId)
    self.externalWindow.setFlags(QtGui.Qt.FramelessWindowHint)
    self.container = QtWidgets.QWidget.createWindowContainer(self.externalWindow)
    self.mainWindow.setCentralWidget(self.container)

    # Install event filters on all Qt objects
    self.externalWindow.installEventFilter(self)
    self.container.installEventFilter(self)
    self.mainWindow.installEventFilter(self)
    self.app.installEventFilter(self)

    self.app.exec_()

  def eventFilter(self, obj, event):
    # Lots of events fire, but no the Close one
    print(str(event.type())) 
    if event.type() == QtCore.QEvent.Close:
      mainWindow.close()
    return False

prevent_garbage_collection = MyApp()
4

1 回答 1

3

下面是一个简单的演示脚本,展示了如何检测嵌入式外部窗口何时关闭。该脚本仅适用于 Linux/X11。要运行它,您必须安装wmctrl。解决方案本身完全不依赖 wmctrl:它只是用来从进程 ID 中获取窗口 ID;我只在我的演示脚本中使用了它,因为它的输出很容易解析。

实际的解决方案依赖于QProcess。这用于启动外部程序,然后它的完成信号通知主窗口程序已关闭。目的是这种机制应该取代您当前使用子进程和轮询的方法。这两种方法的主要限制是它们不适用于将自身作为后台任务运行的程序。然而,我在我的 Arch Linux 系统上用一些应用程序测试了我的脚本——包括 Inkscape、GIMP、GPicView、SciTE、Konsole 和 SMPlayer——它们的行为都符合预期(即它们在退出时关闭了容器窗口)。

注意:为了使演示脚本正常工作,可能需要在某些程序中禁用闪屏等,以便它们可以正确嵌入。例如,GIMP 必须像这样运行:

$ python demo_script.py gimp -s

如果脚本抱怨找不到程序 ID,这可能意味着程序作为后台任务启动了自己,因此您必须尝试找到某种方法将其强制到前台。


免责声明:上述解决方案可能适用于其他平台,但我没有在那里测试过,因此无法提供任何保证。我也不能保证它适用于 Linux/X11 上的所有程序。

我还应该指出,嵌入外部第三方窗口不受 Qt 官方支持createWindowContainer函数仅适用于 Qt 窗口 ID,因此外部窗口 ID 的行为是严格未定义的(参见:QTBUG -44404)。这篇 wiki 文章中记录了各种问题:Qt 和外部窗口。特别是,它指出

我们当前 API 的一个更大的问题(尚未讨论)是 QWindow::fromWinId() 返回一个 QWindow 指针,从 API 合约的角度来看,它应该支持任何其他 QWindow 支持的任何操作,包括使用 setter 来操作窗口,并连接到信号以观察窗口的变化。

我们的任何平台在实践中都没有遵守这个合同,并且 QWindow::fromWinId() 的文档没有提到任何关于这种情况的内容。

这种未定义/平台特定行为的原因很大程度上归结为我们的平台依赖于完全控制本机窗口句柄,而本机窗口句柄通常是本机窗口句柄类型的子类,我们在其中实现回调和其他逻辑。当用我们无法控制的实例替换本机窗口句柄时,并且它没有实现我们的回调逻辑,与常规 QWindow 相比,行为变得未定义并且充满漏洞。

因此,在设计依赖此功能的应用程序时,请牢记所有这些,并相应地调整您的期望......


演示脚本

import sys, os, shutil
from PySide2.QtCore import (
    Qt, QProcess, QTimer,
    )
from PySide2.QtGui import (
    QWindow,
    )
from PySide2.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QMessageBox,
    )

class Window(QWidget):
    def __init__(self, program, arguments):
        super().__init__()
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)
        self.external = QProcess(self)
        self.external.start(program, arguments)
        self.wmctrl = QProcess()
        self.wmctrl.setProgram('wmctrl')
        self.wmctrl.setArguments(['-lpx'])
        self.wmctrl.readyReadStandardOutput.connect(self.handleReadStdOut)
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.setInterval(25)
        self.timer.timeout.connect(self.wmctrl.start)
        self.timer.start()
        self._tries = 0

    def closeEvent(self, event):
        for process in self.external, self.wmctrl:
            process.terminate()
            process.waitForFinished(1000)

    def embedWindow(self, wid):
        window = QWindow.fromWinId(wid)
        widget = QWidget.createWindowContainer(
            window, self, Qt.FramelessWindowHint)
        self.layout().addWidget(widget)

    def handleReadStdOut(self):
        pid = self.external.processId()
        if pid > 0:
            windows = {}
            for line in bytes(self.wmctrl.readAll()).decode().splitlines():
                columns = line.split(maxsplit=5)
                # print(columns)
                # wid, desktop, pid, wmclass, client, title
                windows[int(columns[2])] = int(columns[0], 16)
            if pid in windows:
                self.embedWindow(windows[pid])
                # this is where the magic happens...
                self.external.finished.connect(self.close)
            elif self._tries < 100:
                self._tries += 1
                self.timer.start()
            else:
                QMessageBox.warning(self, 'Error',
                    'Could not find WID for PID: %s' % pid)
        else:
            QMessageBox.warning(self, 'Error',
                'Could not find PID for: %r' % self.external.program())

if __name__ == '__main__':

    if len(sys.argv) > 1:
        if shutil.which(sys.argv[1]):
            app = QApplication(sys.argv)
            window = Window(sys.argv[1], sys.argv[2:])
            window.setGeometry(100, 100, 800, 600)
            window.show()
            sys.exit(app.exec_())
        else:
            print('could not find program: %r' % sys.argv[1])
    else:
        print('usage: python %s <external-program-name> [args]' %
              os.path.basename(__file__))
于 2021-01-22T19:02:38.287 回答