简体   繁体   English

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

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

I'm embedding another window into a Qt widget using PySide2.QtGui.QWindow.fromWinId(windowId) .我正在使用PySide2.QtGui.QWindow.fromWinId(windowId)将另一个 window 嵌入到 Qt 小部件中。 It works well, but it does not fire an event when the the original X11 window destroys it.它运行良好,但当原始 X11 window 破坏它时,它不会触发事件。

If I run the file below with mousepad & python3 embed.py and press Ctrl + Q , no event fires and I'm left with an empty widget.如果我使用mousepad & python3 embed.py运行下面的文件并按Ctrl + Q ,则不会触发任何事件,并且我只剩下一个空小部件。

How can I detect when the X11 window imported by QWindow.fromWinId is destroyed by its creator?如何检测由QWindow.fromWinId导入的 X11 window 何时被其创建者销毁?

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

Below is a simple demo script that shows how to detect when an embedded external window closes.下面是一个简单的演示脚本,展示了如何检测嵌入式外部 window 何时关闭。 The script is only intended to work on Linux/X11.该脚本仅适用于 Linux/X11。 To run it, you must have wmctrl installed.要运行它,您必须安装wmctrl The solution itself doesn't rely on wmctrl at all: it's merely used to get the window ID from the process ID;解决方案本身根本不依赖 wmctrl:它只是用来从进程 ID 中获取 window ID; I only used it in my demo script because its output is very easy to parse.我只在我的演示脚本中使用了它,因为它的 output 非常容易解析。

The actual solution relies on QProcess .实际的解决方案依赖于QProcess This is used to start the external program, and its finished signal then notifies the main window that the program has closed.这用于启动外部程序,然后它的完成信号通知主 window 程序已关闭。 The intention is that this mechanism should replace your current approach of using subprocess and polling.目的是这种机制应该取代您当前使用子进程和轮询的方法。 The main limitation of both these approaches is they will not work with programs that run themselves as background tasks .这两种方法的主要限制是它们不适用于将自身作为后台任务运行的程序 However, I tested my script with a number applications on my Arch Linux system - including Inkscape, GIMP, GPicView, SciTE, Konsole and SMPlayer - and they all behaved as expected (ie they closed the container window when exiting).然而,我在我的 Arch Linux 系统上测试了我的脚本——包括 Inkscape、GIMP、GPicView、SciTE、Konsole 和 SMPlayer——它们的行为都符合预期(即它们在退出时关闭了容器 window)。

NB: for the demo script to work properly, it may be necessary to disable splash-screens and such like in some programs so they can embed themselves correctly.注意:为了使演示脚本正常工作,可能需要在某些程序中禁用闪屏等,以便它们可以正确嵌入。 For example, GIMP must be run like this:例如,GIMP 必须像这样运行:

$ python demo_script.py gimp -s

If the script complains that it can't find the program ID, that probably means the program launched itself as a background task, so you will have to try to find some way to force it into the foreground.如果脚本抱怨找不到程序 ID,这可能意味着程序作为后台任务启动了自己,因此您必须尝试找到某种方法将其强制到前台。


Disclaimer : The above solution may work on other platforms, but I have not tested it there, and so cannot offer any guarantees.免责声明:上述解决方案可能适用于其他平台,但我没有在那里测试过,因此无法提供任何保证。 I also cannot guarantee that it will work with all programs on Linux/X11.我也不能保证它适用于 Linux/X11 上的所有程序。

I should also point out that embedding external, third-party windows is not officially supported by Qt .我还应该指出,嵌入外部的第三方 windows 不受 ZE8801102A40AD89DDCFCFDCAEBF008D25Z 的官方支持 The 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 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 .这篇 wiki 文章记录了各种问题: Qt 和国外 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.我们当前 API 的一个更大的问题(尚未讨论)是 QWindow::fromWinId() 返回一个 QWindow 指针,从 API 合约的角度来看,它应该支持任何其他 QWindow 支持的任何操作,包括使用设置器操作 window,并连接到信号以观察 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.我们的任何平台在实践中都没有遵守这个合同,并且 QWindow::fromWinId() 的文档没有提到任何关于这种情况的内容。

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.这种未定义/平台特定行为的原因很大程度上归结为我们的平台依赖于完全控制本机 window 句柄,而本机 window 句柄通常是本机 window 句柄的子类,我们在其中实现了其他逻辑。 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.当用我们无法控制的实例替换本机 window 句柄时,它不实现我们的回调逻辑,与常规 QWindow 相比,行为变得未定义且充满漏洞。

So, please bear all that in mind when designing an application that relies on this functionality, and adjust your expectations accordingly...因此,在设计依赖此功能的应用程序时,请牢记所有这些,并相应地调整您的期望......


The Demo script :演示脚本

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM