【问题标题】:How to attach and detach an external app with PyQT5 or dock an external application?如何使用 PyQT5 附加和分离外部应用程序或停靠外部应用程序?
【发布时间】:2019-11-22 18:01:01
【问题描述】:

我正在使用 ROS 为多机器人系统开发 GUI,但我在界面中最不想做的事情是冻结:在我的应用程序中嵌入 RVIZ、GMAPPING 或其他屏幕。我已经在界面中放置了一个终端,但我无法解决如何将外部应用程序窗口添加到我的应用程序。我知道 PyQt5 有 createWindowContainer,它使用窗口 ID 来停靠外部应用程序,但我没有找到任何示例来帮助我。

如果可能,我想在我的应用程序的选项卡式框架内拖放一个外部窗口。但是,如果这是不可能的或太难了,我可以在单击按钮后仅在选项卡式框架内打开窗口。

我已经尝试打开类似于终端方法的窗口(参见下面的代码),但 RVIZ 窗口在我的应用程序之外打开。

已经尝试使用 wmctrl 命令将attaching/detaching code 代码翻译到 linux,但没有成功。见my code here

也已经尝试过rviz Python Tutorial,但我收到了错误:

Traceback(最近一次调用最后一次): 文件“rvizTutorial.py”,第 23 行,在 导入rviz 文件“/opt/ros/indigo/lib/python2.7/dist-packages/rviz/init.py”,第 19 行,在 导入 librviz_shiboken ImportError:没有名为 librviz_shiboken 的模块

#  Frame where i want to open the external Window embedded
self.Simulation = QtWidgets.QTabWidget(self.Base)
self.Simulation.setGeometry(QtCore.QRect(121, 95, 940, 367))
self.Simulation.setTabPosition(QtWidgets.QTabWidget.North)
self.Simulation.setObjectName("Simulation")
self.SimulationFrame = QtWidgets.QWidget()
self.SimulationFrame.setObjectName("SimulationFrame")
self.Simulation.addTab(rviz(), "rViz")

# Simulation Approach like Terminal
class rviz(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(rviz, self).__init__(parent)
        self.process = QtCore.QProcess(self)
        self.rvizProcess = QtWidgets.QWidget(self)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.rvizProcess)
        # Works also with urxvt:
        self.process.start('rViz', [str(int(self.winId()))])
        self.setGeometry(121, 95, 940, 367)

【问题讨论】:

  • 我曾将外来窗口嵌入到 Qt 小部件中,我不得不警告你:这是一团糟。您必须面对库版本、python 模块不一致以及最重要的窗口管理器 X11 实现问题;您还需要同时使用 Qt 和 GTK python 绑定。 但是。如果您的程序要在“受限”环境中运行(如“始终使用相同的 wm 和几乎相同的 python 模块配置”),那么有一条出路:您只需要弄清楚您的环境并确保它是持久的。如果您可以向我们提供更多相关信息,我很乐意为您提供帮助。
  • 感谢您的回复!是的,我的程序将在“受限”环境中运行。我只会在 Ubuntu 14.04 和 ROS Indigo 上使用 Python 2.7。即使我换了电脑,模块配置也是一样的。您需要什么信息?

标签: python qt ubuntu pyqt5 ros


【解决方案1】:

我没有对此进行具体测试,因为我有一个旧版本的 Qt5,我现在无法升级,而从 Qt5 5.10 startDetached 也返回 pid 以及启动过程的 bool 结果。 在我的测试中,我在开始等待创建窗口的 while 循环之前手动设置了 procId(通过静态 QInputBox.getInt())。 显然还有其他方法可以做到这一点(并获取窗口的 xid)。

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
import gi
gi.require_version('Wnck', '3.0')
from gi.repository import Wnck, Gdk


class Container(QtWidgets.QTabWidget):
    def __init__(self):
        QtWidgets.QTabWidget.__init__(self)
        self.embed('xterm')

    def embed(self, command, *args):
        proc = QtCore.QProcess()
        proc.setProgram(command)
        proc.setArguments(args)
        started, procId = proc.startDetached()
        if not started:
            QtWidgets.QMessageBox.critical(self, 'Command "{}" not started!')
            return
        attempts = 0
        while attempts < 10:
            screen = Wnck.Screen.get_default()
            screen.force_update()
            # this is required to ensure that newly mapped window get listed.
            while Gdk.events_pending():
                Gdk.event_get()
            for w in screen.get_windows():
                if w.get_pid() == procId:
                    window = QtGui.QWindow.fromWinId(w.get_xid())
                    container = QtWidgets.QWidget.createWindowContainer(window, self)                    
                    self.addTab(container, command)
                    return
            attempts += 1
        QtWidgets.QMessageBox.critical(self, 'Window not found', 'Process started but window not found')


app = QtWidgets.QApplication(sys.argv)
w = Container()
w.show()
sys.exit(app.exec_())

【讨论】:

  • 感谢您的帮助!当我尝试安装“gi”框架时,我遇到了一些问题。当我尝试使用 $ pip install PyGObject 时会出现错误 Requested 'glib-2.0 >= 2.48.0' 但 GLib 的版本是 2.40.2。已经尝试了 sudo apt-get update && sudo apt-get upgrade ,错误依旧。也许是因为我使用的是旧版本的linux? (Ubuntu 值得信赖)
  • 您已被阻止进入 gtk 更新虫洞。我认为您仍然可以使用 wnck 模块的第 2 版(注意小写)。据我所知,您不应该需要版本 2 中的 Gdk.event_get,但是,即使它会使用不同的模块命名方案。但是,您仍然可以忽略所有这些并继续使用纯 shell/popen 实现:虽然不那么“优雅”,但它更可靠,只要控制台程序对应物按预期反应。
  • 再次感谢您的帮助!我尝试了一切,但找不到运行代码的解决方案,因为由于某种原因我无法安装 PyGObject(所以我无法导入 gi 模块)。我稍微修改了你的代码,你可以看到here。但是我遇到了一个错误,请您看看我的代码是否有问题?
  • gi 要求仅适用于版本 3(以确保在安装更多版本时加载正确的版本),如果您只使用版本 2,则不需要它。我看过另一个问题(供以后参考,由于主题相同,只需更新现有问题,不要创建新问题):您是否尝试过自行手动启动rviz,获取其窗口ID和然后通过硬编码 id(或作为命令参数)来创建基本嵌入?
  • 再次感谢您的回答和提示。是的,我已经尝试单独打开 rviz,通过 shell 使用 wmctrl 获取他的 ID,并使用从 PyQT 解析为 winID(我复制并粘贴值)类的他的 ID,也不起作用。我认为这可能是我的代码,因为有些人在 windows 中使用 win32gui 模块这样做。我只是不知道如何在 linux 环境中做同样的事情。
【解决方案2】:

我无法获得在 Ubuntu 18.04.3 LTS 上工作的已接受答案中的代码;即使我摆脱了阻止代码运行的异常,我仍然会得到一个单独的 PyQt5 窗口和单独的 xterm 窗口。

经过一番尝试,我终于在标签页内打开了xterm 窗口;这是我在 Ubuntu 18.04.3 LTS 中工作的代码(所有错误都已注释):

#!/usr/bin/env python3
# (same code seems to run both with python3 and python2 with PyQt5 in Ubuntu 18.04.3 LTS)
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
import gi
gi.require_version('Wnck', '3.0')
from gi.repository import Wnck, Gdk
import time

class Container(QtWidgets.QTabWidget):
    def __init__(self):
        QtWidgets.QTabWidget.__init__(self)
        self.embed('xterm')

    def embed(self, command, *args):
        proc = QtCore.QProcess()
        proc.setProgram(command)
        proc.setArguments(args)
        #started, procId = proc.startDetached()
        #pid = None
        #started = proc.startDetached(pid)
        # https://stackoverflow.com/q/31519215 : "overload" startDetached : give three arguments, get a tuple(boolean,PID)
        # NB: we will get a failure `xterm: No absolute path found for shell: .` even if we give it an empty string as second argument; must be a proper abs path to a shell
        started, procId = proc.startDetached(command, ["/bin/bash"], ".")
        if not started:
            QtWidgets.QMessageBox.critical(self, 'Command "{}" not started!'.format(command), "Eh")
            return
        attempts = 0
        while attempts < 10:
            screen = Wnck.Screen.get_default()
            screen.force_update()
            # do a bit of sleep, else window is not really found
            time.sleep(0.1)
            # this is required to ensure that newly mapped window get listed.
            while Gdk.events_pending():
                Gdk.event_get()
            for w in screen.get_windows():
                print(attempts, w.get_pid(), procId, w.get_pid() == procId)
                if w.get_pid() == procId:
                    self.window = QtGui.QWindow.fromWinId(w.get_xid())
                    #container = QtWidgets.QWidget.createWindowContainer(window, self)
                    proc.setParent(self)
                    #self.scrollarea = QtWidgets.QScrollArea()
                    #self.container = QtWidgets.QWidget.createWindowContainer(self.window)
                    # via https://vimsky.com/zh-tw/examples/detail/python-method-PyQt5.QtCore.QProcess.html
                    #pid = proc.pid()
                    #win32w = QtGui.QWindow.fromWinId(pid) # nope, broken window
                    win32w = QtGui.QWindow.fromWinId(w.get_xid()) # this finally works
                    win32w.setFlags(QtCore.Qt.FramelessWindowHint)
                    widg = QtWidgets.QWidget.createWindowContainer(win32w)

                    #self.container.layout = QtWidgets.QVBoxLayout(self)
                    #self.addTab(self.container, command)
                    self.addTab(widg, command)
                    #self.scrollarea.setWidget(self.container)
                    #self.container.setParent(self.scrollarea)
                    #self.scrollarea.setWidgetResizable(True)
                    #self.scrollarea.setFixedHeight(400)
                    #self.addTab(self.scrollarea, command)
                    self.resize(500, 400) # set initial size of window
                    return
            attempts += 1
        QtWidgets.QMessageBox.critical(self, 'Window not found', 'Process started but window not found')


app = QtWidgets.QApplication(sys.argv)
w = Container()
w.show()
sys.exit(app.exec_())

【讨论】:

    猜你喜欢
    • 2019-06-20
    • 1970-01-01
    • 2014-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多