PDA

View Full Version : Show widget with PySide2 embedded in Qt5 application



tuli
13th February 2020, 22:47
Hi,

I have embedded a Python interpreter in my Qt5.12.0 App. The interpreter is python 3.6, running on Ubuntu 18.04. Everything works as expected.
But now I want to show a widget from a python script inside my application, with PySide2.



import sys
from PySide2.QtWidgets import QApplication, QLabel

app = QApplication(sys.argv)
label = QLabel("<font color=red size=40>Hello World!</font>")
label.show()
app.exec_()

This works fine as a standalone PySide2 Qt application. But obviously I cant create another QApplication in my embedded interpreter, because my regular C++ application already does that.
However, just doing


label = QLabel("<font color=red size=40>Hello World!</font>")
label.show()

doesnt work, which doesnt really surprise me.



Question: How can I make this work?


(Final goal would be to create a docking widget from python, so I would have to make it a child of my QMainWindow, or expose a C++ baseclass to derive from. But that's the next step I guess...)

d_stranz
14th February 2020, 01:55
In C++ you can access the global QApplication instance using qApp (or equivalently, QCoreApplication::instance()). Would that help (or work) in PySide?

Another option - can you expose a QWindow from python that you could use to create a QWidget in C++ (QWidget::createWindowContainer()) or vice-versa?

Just sort of guessing here. I know you can expose C++ bindings to python, so it would seem logical that you could expose a widget from the Qt side that could be used in a python app as a container for a python-side created child.

Edit: Check out this stackoverflow post (https://stackoverflow.com/questions/22553642/embeding-pyside-pyqt-widget-into-qt-c-application).

tuli
14th February 2020, 06:53
Thanks!

After modifying the stackoverflow code to work with PySide2, it works fine for displaying a QMessageBox. For any QWidget derived item, the application crashes upon trying to draw it (so immediately), deep inside painting code.


from PySide2.QtWidgets import QApplication, QLabel, QMessageBox, QPushButton

all_widgets = QApplication.instance().allWidgets()
window = None
for widget in all_widgets:
if str(widget.objectName()) == "main_window":
window = widget
break

//works
QMessageBox.information(window, 'Test', 'Hello!', QMessageBox.Ok)

//crash during paint
label = QLabel("<font color=red size=40>Hello World!</font>")
label.setParent(window)
label.show()


backtrace: https://pastebin.com/d3qTSgef


Smells a bit like a use-after-free, maybe there is an issue with having to specify that I take ownership of the widget or something ...

Thanks again :)

d_stranz
14th February 2020, 15:51
it works fine for displaying a QMessageBox. For any QWidget derived item, the application crashes

So, the big difference between QMessageBox and QWidget is that the message box is modal and runs its own event loop. Everything is self-contained and it does not need to pass events across the python - C++ border. I think the only thing the "window" argument does in the QMessageBox method is to tell Qt to center the message box on that window. It isn't establishing a QWidget parent-child relationship because QMessageBox::information() is a static method so therefore there is no actual QWidget instance created (i.e. for static methods of a class, no instance of the class is required to execute it. In this case, QMessageBox is equivalent to enclosing the method in a namespace).


Smells a bit like a use-after-free

If your code above represents a stand-alone python executable, then I would say that is the case. You create your QLabel, parent it with a widget on the C++ side, show it, and then your python program exits leaving an invalid pointer on the C++ side. Normally, calling setParent() would connect the parent to the child's destroyed() signal so the ownership could be cleaned up, but it seems like there is some synchronization problem between the two sides.

Is there some way to keep your python program running? You basically want the C++ side to treat it like a DLL - it gets loaded and stays loaded until it is no longer needed.

tuli
20th February 2020, 10:44
Unfortunately there is no way to keep it running.

Meanwhile i have found a somewhat complicated way. Turns out the PySide2+shiboken2+shiboken2_generator supports this.
In fact there is an example in pyside_setup/examples/scriptableapplication that does what I want!

Unfortunately the setup is somewhat complicated and timeconsuming. Essentially you have to build the three packages from source yourself, specifying the Qt installation (needs sources installed) you want to use those packages for. Then the build process will generate python bindings for every last Qt class automatically. This step takes an hour on my computer. Once the packages are installed (they need to be in a virtualenv btw) you are supposed to use the magic cmake file from the example which does a ton of magic and then it works with the example....

So this proves it is possible.

But the price is high. There are prebuild packages on their server, but they explicitly recommend to build from sources, and indeed i couldnt get the precompiled stuff to work at all. All this makes it hard to ship it to endusers.


If someone has any more smart ideas ... they are welcome!

d_stranz
20th February 2020, 20:28
In reading the python documentation on embedding (https://docs.python.org/3/extending/embedding.html) it would seem (from section 1.4) that you could expose your main window pointer to the python interpreter, thereby permitting a python script from creating a widget owned by the QMainWindow instance.

This keeps the Qt app as the driver for the script and should allow you to create external scripts that are basically nothing more than functions that the interpreter loads. Your previous approach seems to want to make the external python script the driver instead - basically not even using the embedded interpreter.

Is this not doable? You might be able to adapt the Qt Plugin architecture for this - your python interpreter loads the python script and returns a QObject pointer to the python class that implements the plugin interface. The interface functionality is exposed by your app to define the protocol for the conversation between the python script and the Qt app.