如何检测嵌入QWidget.createWindowContainer的外部窗口何时关闭? [英] How to detect when a foreign window embedded with QWidget.createWindowContainer closes itself?

查看:83
本文介绍了如何检测嵌入QWidget.createWindowContainer的外部窗口何时关闭?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm embedding another window into a Qt widget using PySide2.QtGui.QWindow.fromWinId(windowId). It works well, but it does not fire an event when the the original X11 window destroys it.

如果我使用 mousepad&python3 embed.py 并按 Ctrl + Q ,则不会触发任何事件,而我留下一个空的小部件.

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.

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

How can I detect when the X11 window imported by QWindow.fromWinId is destroyed by its creator?

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

推荐答案

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

Below is a simple demo script that shows how to detect when an embedded external window closes. The script is only intended to work on Linux/X11. To run it, you must have wmctrl installed. The solution itself doesn't rely on wmctrl at all: it's merely used to get the window ID from the process ID; I only used it in my demo script because its output is very easy to parse.

实际解决方案取决于 QProcess .这用于启动外部程序,然后其完成信号然后通知主窗口程序已关闭.目的是该机制应替代您当前使用子过程和轮询的方法.这两种方法的主要局限性是它们不适用于以后台任务运行的程序.但是,我在Arch Linux系统上的多个应用程序(包括Inkscape,GIMP,GPicView,SciTE,Konsole和SMPlayer)上测试了脚本,它们的行为均与预期的一样(即,退出时它们关闭了容器窗口).

The actual solution relies on QProcess. This is used to start the external program, and its finished signal then notifies the main window that the program has closed. 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 (i.e. they closed the container window when exiting).

NB:为了使演示脚本正常工作,可能有必要禁用启动屏幕等,例如在某些程序中,以便它们可以正确地嵌入自身.例如,GIMP必须这样运行:

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:

$ python demo_script.py gimp -s

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

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.

免责声明:上述解决方案可能在其他平台上也可以使用,但我尚未在该平台上进行过测试,因此无法提供任何保证.我也不能保证它能与Linux/X11上的所有程序一起使用.

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.

我还要指出, 嵌入外部第三方窗口并不受Qt的正式支持 . createWindowContainer 函数仅适用于Qt窗口ID,因此带有外部窗口ID的行为是完全未定义的(请参阅: Qt和国外窗口.特别是,声明:

I should also point out that embedding external, third-party windows is not officially supported by Qt. 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. In particular, it states:

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

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.

我们的任何平台在实践中均未遵守本合同,而且QWindow :: fromWinId()的文档没有提到关于情况的任何事情.

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相比.

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.

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

So, please bear all that in mind when designing an application that relies on this functionality, and adjust your expectations accordingly...

演示脚本:

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

这篇关于如何检测嵌入QWidget.createWindowContainer的外部窗口何时关闭?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆