1

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)
Ircbarros
  • 945
  • 8
  • 15
  • I had my share of embedding foreign windows into Qt widgets, and I'd have to warn you: it's a mess. You've to face library versions, python module inconsistency and most of all window manager X11 implementation issues; also you'll need to use both Qt and GTK python bindings. *But*. If your program is going to run in a "confined" environment (as in "always the same wm and almost the same python module configuration") there's a way out: you only need to figure out your environment and ensure that's persistent. If you can give us more information about that, I'd be glad to help you with it. – musicamante Jul 13 '19 at 22:29
  • Thank's for the reply! Yes, my program will run in a "confined" environment. I will only use with Python 2.7, on Ubuntu 14.04 and ROS Indigo. Even if i change the computer, the module configuration will be the same. What information do you need? – Ircbarros Jul 14 '19 at 00:19

2 Answers2

2

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_())
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks for your help! I'm facing some problems when i try to install the "gi" framework. When i try to use **$ pip install PyGObject** gives the error **Requested 'glib-2.0 >= 2.48.0' but version of GLib is 2.40.2**. Already tried the sudo apt-get update && sudo apt-get upgrade, and the error stills. Maybe because i'm in an old version of linux? (Ubuntu Trusty) – Ircbarros Jul 18 '19 at 02:04
  • You've got yourself blocked into the gtk update wormhole. I think you can still use version 2 of wnck module (be careful about the lowercase). As far as I can remember you shouldn't need the Gdk.event_get in version 2, but, even if, it would use a different module naming scheme. But, still, you can just ignore all that and go on with the pure shell/popen implementation: while being less "elegant", it's much more reliable, as long as the console program counterparts react as expected. – musicamante Jul 18 '19 at 03:30
  • Thank you again for all your help! I tried everything, but can't find a solution to run your code, because i'm unable to install the PyGObject (so i cant import the gi module) for some reason. I modified your code a little, as you can se [here](https://stackoverflow.com/questions/57085854/run-an-active-window-inside-an-pyqt5-gui-application). But i'm getting an error, could you see if there's something wrong in my code please? – Ircbarros Jul 18 '19 at 14:38
  • the `gi` requirement is only for version 3 (to ensure it loads the correct version if more versions are installed), if you are only using version 2 you don't need it. I've seen the other question (for future reference, since the topic is the same, just update the existing question, don't create a new one): have you tried to manually launch rviz on its own, get its window id and then create a basic embed by hardcoding the id (or as a command argument)? – musicamante Jul 18 '19 at 19:38
  • Thank you again for your answer and also for the tip. Yes, i already tried to open the rviz alone, adquiring his ID with the wmctrl via shell and used his ID parsed to winID (i copy and pasted the value) class from PyQT, doesn't work either. I think that maybe is my code, because some people do that in windows using the win32gui module. I just don't know how i do the same for a linux environment. – Ircbarros Jul 19 '19 at 00:11
  • Is strange because i can embed the urxvt using the approach finded [here](https://stackoverflow.com/questions/51975678/embedding-a-terminal-in-pyqt5), but if the command changes to *self.process.start('rviz', str(int(self.terminal.winId())))* don't work. The window open outside the application or gives the error "rviz is not a process". – Ircbarros Jul 19 '19 at 00:17
2

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

sdaau
  • 36,975
  • 46
  • 198
  • 278