如何检测嵌入 QWidget.createWindowContainer 的外来 window 何时自行关闭?

How to detect when a foreign window embedded with QWidget.createWindowContainer closes itself?

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

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

我如何检测 QWindow.fromWinId 导入的 X11 window 何时被其创建者销毁?

#!/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()

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

实际的解决方案依赖于QProcess. This is used to start the external program, and its finished signal然后通知主程序window程序已经关闭。目的是这种机制应该取代您当前使用子流程和轮询的方法。这两种方法的主要限制是它们 无法与本身作为后台任务 运行 的程序一起使用 。然而,我在我的 Arch Linux 系统上用一些应用程序测试了我的脚本——包括 Inkscape、GIMP、GPicView、SciTE、Konsole 和 SMPlayer——它们都按预期运行(即它们关闭了容器 window退出时)。

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

$ python demo_script.py gimp -s

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


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

上的所有程序

我还应该指出,Qt 不正式支持 嵌入外部第三方 windows。 createWindowContainer function is only intended to work with Qt window IDs, so the behaviour with foreign window IDs is strictly undefined (see: QTBUG-44404). The various issues are documentented in this wiki article: Qt and foreign windows. In particular, it states:

A larger issue with our current APIs, that hasn't been discussed yet, is the fact that QWindow::fromWinId() returns a QWindow pointer, which from an API contract point of view should support any operation that any other QWindow supports, including using setters to manipulate the window, and connecting to signals to observe changes to the window.

This contract is not adhered to in practice by any of our platforms, and the documentation for QWindow::fromWinId() doesn't mention anything about the situation.

The reasons for this undefined/platform specific behaviour largely boils down to our platforms relying on having full control of the native window handle, and the native window handle often being a subclass of the native window handle type, where we implement callbacks and other logic. When replacing the native window handle with an instance we don't control, and which doesn't implement our callback logic, the behaviour becomes undefined and full of holes compared to a regular 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__))