微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何检测嵌入 QWidget.createWindowContainer 的外部窗口何时关闭?

如何解决如何检测嵌入 QWidget.createWindowContainer 的外部窗口何时关闭?

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

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

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

Screenshots of the existing Mousepad window,of the mousepad window embedded in the embed.py frame,and of the empty embed.py frame

#!/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;我只在我的演示脚本中使用它,因为它的输出很容易解析。

实际的解决方案依赖于 QProcess。这用于启动外部程序,然后它的 finished signal 通知主窗口该程序已关闭。目的是这种机制应该取代您当前使用子流程和轮询的方法。这两种方法的主要限制是它们不适用于将自身作为后台任务运行的程序。但是,我在我的 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 and foreign windows。特别是,it states

我们当前 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)
        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,'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__))

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。