PythonQt properties shadowing methods

Hi all, I’ve noticed a strange behavior that I could not understand if it is a bug or intended.
It seems to me that a property can sometimes shadow methods from qt objects. Example:

qgw = qt.QGraphicsWidget()

qgw.layout is None

qgw.setLayout(qt.QGraphicsLayout())

This runs ok with no errors, and even emit the signal layoutChanged
but qgw.layout continues to be None

if I try to

qgw.layout = qt.QGraphicsLayout()

I get an error:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: Property 'layout' of type 'QGraphicsLayout*' does not accept an object of type QGraphicsLayout (QGraphicsLayout (C++ object at: 0x000002187FBE0B90))

Which I can understand, because setting an object to a pointer variable can only result in a error. But the layout() method is not accessible, which is also understandable from the python point of view.

That is not the case for QWidget, which I can access the layout via layout() slot. Can you help me to understand what is happening? I know it may be more related to PythonQt than Slicer, but I wanted to use QGraphicsWidget with another lib. Thanks

For reference, discussions on github:

There are hundreds of graphing libraries for Python. pyqtgraph may make sense if you already use the same Qt wrapping as it does, but otherwise bringing in an entire new set of Qt is probably not worth it.

1 Like

Thanks Andras, your input is always greatly appreciated. We’ve managed to use matplotlib inside slicer to improve plotting capabilities, but we need a lib that allow us to plot things more dynamically and interactively. Also we have another project outside slicer that we built based on pyqtgraph that we would like to include into slicer.

I’ve managed to install PySide2 in Slicer and run pyqtgraph with it, but as expected the python wrapped qt objects are not compatible with pyqhtonqt wrapped ones. i.e. I cannot add a PySide2.QtCore.QWidget to a PythonQt.QtGui.QLayout. This way I can only use pyqtgraph as a separate window. A wrapper of some kind comes to mind to make pythonqt accept a pyside2 widget, but I don’t know how to start looking into it.

It sounds like you are into a potential dangerzone here by mixing PythonQt with PySide2, but if it’s working and matches your use case, I guess we can’t stop you ; )

If you just need a workaround, you should be able to get the pixel data from any of your widgets as numpy arrays that you can then display in the “other” qt.

For example in SlicerWeb I do:

    slicer.qMRMLUtils().qImageToVtkImageData(timeImage, vtkTimeImage)

then I get a numpy array from the vtkImageData. It should be a very fast operation, even if it is a “creative” (hacky) workaround.

Thanks @pieper that is a nice workaround to know as well.

I’ve come up with this other hack so far:

from PySide2.QtWidgets import QVBoxLayout
import shiboken2
pysidelayout = shiboken2.wrapInstance(hash(self.layout), QVBoxLayout)
import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
cw = pg.GraphicsLayoutWidget()
pysidelayout.addWidget(cw)

This way I can wrap a pythonqt qt object with shiboken and use it as it were a pyside2 object. Events are working, nicely and I got to add a pyside2 widget to a slicer layout. I will report if I find any issues, so far so good.

The other way around is still a mystery (pyside2 → pythonqt).

1 Like

Reporting back on the hack of the post above. It works at first when creating and displaying a widget from pyside2. But when the parent widget gets destroyed, Slicer crashes. I’ve tried to remove my widget from the Slicer layout before the destruction of the PythonQt object with no success. I am guessing that the events are processed at the C++ level before getting passed to python for me to get a chance to remove the widget.

There are two options for me to go further with this madness:
1 - Figure why some functions are missing from PythonQt
2 - Subclass QLayout in C++ for it not to take ownership of its widgets so that I can manage them from python. i.e. create widget with pyside, add to this unmanaged layout and destroy the widget myself via python when the layout is no more.

Hello everyone. Turns out the crashes were stackoverflows and I sort of fixed the issue with some checks on PythonQt. see ENH: added sanity checks for compatibility with pyqtgraph by fbordignon · Pull Request #81 · commontk/PythonQt · GitHub
Now I have pretty plots with pyqtgraph 0.11.1:

3 Likes

Hey, that’s nice! :+1:

Will you have sample code for people to try?

1 Like

For this example, the user needs to have the python3.dll fix, alternatively one can temporarily download and install WinPython 3.6.7 and copy python3.dll from winpython/python-3.6.7.amd64/python3.dll to Slicer/bin/python3.dll
Slicer can crash if one does not have PythonQt compiled with this fix. It works without it, but Slicer can become unstable.

Working example based on plotting example of pyqtgraph:

# Copy and paste this on Slicer's python terminal
pip_install('pyside2==5.15.1') # must be important to match pyside2 version to slicer's qt version
pip_install('pyqtgraph')

import textwrap
from PySide2.QtWidgets import QVBoxLayout
import shiboken2
import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg

# Custom layout base class
class BaseLayout:
    TAG = "EmptyScreen"
    UID = 1010
    LAYOUT = textwrap.dedent(
        f"""
        <layout type="vertical">
            <item>
                <{TAG}></{TAG}>
            </item>
        </layout>
    """
    )
    @classmethod
    def register(cls):
        viewFactory = slicer.qSlicerSingletonViewFactory()
        viewFactory.setTagName(cls.TAG)
        if slicer.app.layoutManager() is not None:
            slicer.app.layoutManager().registerViewFactory(viewFactory)
        container = cls.build(viewFactory)
        layoutManager = slicer.app.layoutManager()
        layoutManager.layoutLogic().GetLayoutNode().AddLayoutDescription(cls.UID, cls.LAYOUT)
        return container
    @classmethod
    def build(cls, factory):
        viewWidget = qt.QWidget()
        viewWidget.setAutoFillBackground(True)
        factory.setWidget(viewWidget)
        viewLayout = qt.QVBoxLayout()
        viewWidget.setLayout(viewLayout)
        return viewWidget
    @classmethod
    def show(cls):
        slicer.app.layoutManager().setLayout(cls.UID)

#my plot layout class
class PlotLayout(BaseLayout):
    TAG = "MyPlotWidget"
    UID = 1019
    LAYOUT = textwrap.dedent(f"""
        <layout type="vertical">
            <item>
                <{TAG}></{TAG}>
            </item>
        </layout>
    """)

lw = PlotLayout.register().layout()
PlotLayout.show()

# wrap pythonQt instance with shiboken2 (pyside2)
pysidelayout = shiboken2.wrapInstance(hash(lw), QVBoxLayout)

pg.mkQApp()

# start of pyqtgraph plotting example https://github.com/pyqtgraph/pyqtgraph/blob/master/examples/Plotting.py
win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
pysidelayout.addWidget(win)

# Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True)

p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100))

p2 = win.addPlot(title="Multiple curves")
p2.plot(np.random.normal(size=100), pen=(255,0,0), name="Red curve")
p2.plot(np.random.normal(size=110)+5, pen=(0,255,0), name="Green curve")
p2.plot(np.random.normal(size=120)+10, pen=(0,0,255), name="Blue curve")

p3 = win.addPlot(title="Drawing with points")
p3.plot(np.random.normal(size=100), pen=(200,200,200), symbolBrush=(255,0,0), symbolPen='w')

win.nextRow()

p4 = win.addPlot(title="Parametric, grid enabled")
x = np.cos(np.linspace(0, 2*np.pi, 1000))
y = np.sin(np.linspace(0, 4*np.pi, 1000))
p4.plot(x, y)
p4.showGrid(x=True, y=True)

p5 = win.addPlot(title="Scatter plot, axis labels, log scale")
x = np.random.normal(size=1000) * 1e-5
y = x*1000 + 0.005 * np.random.normal(size=1000)
y -= y.min()-1.0
mask = x > 1e-15
x = x[mask]
y = y[mask]
p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50))
p5.setLabel('left', "Y Axis", units='A')
p5.setLabel('bottom', "Y Axis", units='s')
p5.setLogMode(x=True, y=False)

p6 = win.addPlot(title="Updating plot")
curve = p6.plot(pen='y')
data = np.random.normal(size=(10,1000))
ptr = 0

def update():
    global curve, data, ptr, p6
    curve.setData(data[ptr%10])
    if ptr == 0:
        p6.enableAutoRange('xy', False)  ## stop auto-scaling after the first data set is plotted
    ptr += 1

timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(50)


win.nextRow()

p7 = win.addPlot(title="Filled plot, axis disabled")
y = np.sin(np.linspace(0, 10, 1000)) + np.random.normal(size=1000, scale=0.1)
p7.plot(y, fillLevel=-0.3, brush=(50,50,200,100))
p7.showAxis('bottom', False)


x2 = np.linspace(-100, 100, 1000)
data2 = np.sin(x2) / x2
p8 = win.addPlot(title="Region Selection")
p8.plot(data2, pen=(255,255,255,200))
lr = pg.LinearRegionItem([400,700])
lr.setZValue(-10)
p8.addItem(lr)

p9 = win.addPlot(title="Zoom on selected region")
p9.plot(data2)

def updatePlot():
    p9.setXRange(*lr.getRegion(), padding=0)

def updateRegion():
    lr.setRegion(p9.getViewBox().viewRange()[0])

lr.sigRegionChanged.connect(updatePlot)
p9.sigXRangeChanged.connect(updateRegion)
updatePlot()

4 Likes

Wow, that’s very cool :eyes:

Do you think this is a stable setup? It looks like all the events and timers work and there are no other special hacks? If so that would allow people to mix other PySide2 based code in Slicer (e.g. these widgets) or share widgets with napari.

Hey @pieper thanks. We are gonna try to use code we have from another proprietary Pyside2 project inside Slicer in production, hoping that it is stable enough. I will report if we find issues.

I was surprised by the compatibility, TBH I was expecting more of a struggle. But it seems that PythonQt and pyside2/shiboken2 are built with the assumption that objects can be created by C++ and/or Python so they need to keep references updated, i.e. if pyside2 creates objects and adds a child to a PythonQt object, it is more or less like C++ is adding it. Note that this is an impression based on things working and a comprehension of the code tending to zero.

I expect a bit more work now to keep track of the ownership of the objects. Anyways, maybe we can start a new thread to test this.

1 Like

Very nice - I hope it works well in your project! I agree with your logic so, fingers crossed, it should hold up in practice.

1 Like

Hi, I am using this fix for 10 months now. We’ve released 4 stable versions of our custom app to final users without any problems reported regarding this issue. I figure because it is a simple solution, we could merge it and see if someone else is interested. Thanks.

2 Likes