简体   繁体   中英

How to attach and detach an external app with PyQT5 or dock an external application?

I'm developing an GUI for multi-robot system using ROS, but i'm freezing in the last thing i want in my interface: embedding the RVIZ, GMAPPING or another screen in my application. I already put an terminal in the interface, but i can't get around of how to add an external application window to my app. I know that PyQt5 have the createWindowContainer, with uses the window ID to dock an external application, but i didn't find any example to help me with that.

If possible, i would like to drag and drop an external window inside of a tabbed frame in my application. But, if this is not possible or is too hard, i'm good with only opening the window inside a tabbed frame after the click of a button.

I already tried to open the window similar to the terminal approach (see the code bellow), but the RVIZ window opens outside of my app.

Already tried to translate the attaching/detaching code code to linux using the wmctrl command, but didn't work wither. See my code here .

Also already tried the rviz Python Tutorial but i'm receveing the error:

Traceback (most recent call last): File "rvizTutorial.py", line 23, in import rviz File "/opt/ros/indigo/lib/python2.7/dist-packages/rviz/ init .py", line 19, in import librviz_shiboken ImportError: No module named 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)

I've not tested this specifically, as I've an old version of Qt5 I can't upgrade right now, while from Qt5 5.10 startDetached also returns the pid along with the bool result from the started process. In my tests I manually set the procId (through a static QInputBox.getInt()) before starting the while cycle that waits for the window to be created. Obviously there are other ways to do this (and to get the xid of the window).

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_())

I couldn't get the code in the accepted answer to work on Ubuntu 18.04.3 LTS; even when I got rid of the exceptions preventing the code to run, I'd still get a separate PyQt5 window, and separate xterm window.

Finally after some tries, I got the xterm window to open inside the tab; here is my code working in Ubuntu 18.04.3 LTS (with all the misses commented):

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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