【问题标题】:How to detect when a foreign window embedded with QWidget.createWindowContainer closes itself?如何检测嵌入 QWidget.createWindowContainer 的外部窗口何时自行关闭?
【发布时间】:2021-04-25 05:42:33
【问题描述】:

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

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

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

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

【问题讨论】:

  • 这是您在实际程序中启动嵌入式应用程序的方式吗?您实际上是在尝试嵌入 mousepad 还是其他一些应用程序?更现实的测试用例可能更有帮助。您还应该说明您需要在哪些平台上工作。
  • @ekhumoro 我正在嵌入多个应用程序:Inkscape、GIMP、Blender……以及至少一个文本编辑器(尽管可能不是鼠标垫),因此解决方案不应该是特定于应用程序的。我使用Popen 启动应用程序,并过滤xwininfoxprop 的输出以从PID 中获取窗口ID。完整代码在github.com/jsmaniac(还是乱码)+MyX11Utils.py同目录下
  • @ekhumoro 关于平台的要点。由于我将这些外部应用程序嵌入到适用于 Linux、Windows、BSD 和 macos 的 FreeCAD 中,因此这四个将是理想的,但仅 Linux 的 Xorg-only(没有 Wayland)将是一个好的开始。现在我唯一的选择是轮询,这很昂贵。
  • @SuzanneDupéron 虽然可以实现外部窗口嵌入,但它有 很多 问题,具体取决于平台和“版本”(MacOS/Windows 的操作系统版本,发行版/wm /dm 对于 Linux)。其中最重要的一项是键盘焦点处理,它经常会出现一些问题,因为它没有正确获取或释放;几何通常是另一个,因为取决于程序使用的框架,可能会有一些特性(甚至覆盖)假设程序的窗口是“它自己的”,这对于基于停靠小部件的程序来说也可能成为一个更大的问题,例如作为 GIMP。

标签: python python-3.x embed pyside2 qwindow


【解决方案1】:

下面是一个简单的演示脚本,展示了如何检测嵌入式外部窗口何时关闭。该脚本仅适用于 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, 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__))

【讨论】:

  • 应该注意(如预期的那样)这不适用于自行创建其他窗口的程序:例如,我将 GIMP 与单独的窗口一起使用,它显然无法赶上工具窗口,也不是为新图像创建的窗口。我想知道如果使用 wmctrl python 模块(甚至是 Xlib 或 wnck),可能会有更可靠的结果允许其他功能。
  • @musicamante wmctrl 工具仅用于从 procID 获取 winID。你如何做到这一点没有区别。该问题的主要主题是如何检测嵌入程序何时关闭。我的回答是使用 QProcess - 要点是控制程序的启动方式可以让您检测到它何时关闭。但是,我觉得我还应该提供一些额外的上下文来解释为什么这种方法是必要的(并添加一些警告)。我的回答当然不是关于如何嵌入任意外国窗口的通用解决方案!这只是一个简单的演示。
  • 问题的主要主题是如何检测嵌入式窗口何时关闭,因此如果程序有来自同一进程的两个窗口(例如,对于两个单独的文档),此解决方案将仅检测进程终止(通常在最后一个窗口关闭时)。但是,您从 wiki 中引用的内容清楚地表明,不应期望仅使用 Qt 解决方案来检测窗口何时关闭,因此我接受此作为答案。此外,它还包含对我的解决方案的一些改进(例如使用 -s 来抑制启动画面和我未能正确处理的 .setGeometry)。
  • @SuzanneDupéron 感谢您接受我的回答。但是,您的问题和 cmets 没有提及有关单独文档的任何额外细节 - 您甚至说解决方案不应该是特定于应用程序的。您问题中的示例只是嵌入了鼠标垫并尝试检测 Ctrl+Q,所以这就是我的答案的基础。抱歉,你没有完全得到你想要的,但我确实要求你提供一个更现实的例子......
  • 在 Linux/X11 上给定一个 pid,您可以使用 XGetWindowProperty 和 XQueryTree 的组合找到本机窗口 ID。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多