4

I'm porting a pyqt5 GUI to pyside6 and I'm running into an issue I don't understand.

I have two QSpinboxes that control two GUI parameters: One spinbox controls the weight of a splitter, the other controls the row height in a QtableView.

This is the code in pyqt5:

spinbox1.valueChanged.connect(my_splitter.setHandleWidth)
spinbox2.valueChanged.connect(my_view.verticalHeader().setDefaultSectionSize)

In Pyside6 spinbox1 works fine, but spinbox2 doesn't do its job and there is this warning:

You can't add dynamic slots on an object originated from C++.

The issue can be solved by changing the second line of code to:

spinbox2.valueChanged.connect(lambda x: my_view.verticalHeader().setDefaultSectionSize(x))

It's nice to have found a solution, but I would also like to understand why the two connections behave differently in PySide6 and why using he lambda solves the issue.

The warning message probably holds a clue but I have no idea what dynamic slots are (and a quick google didn't help me much).

Edit: Since I was changing two things: Qt5 > QT6, And pyqt > pyside I looked at this in 4 python wrappers (pyqt5, pyqt6, pyside2, pyside6) to see which of the changes caused the issue. And I can tell that both pyside 2 and 6 show this behaviour, and none of the pyqt's

mahkitah
  • 562
  • 1
  • 6
  • 19
  • It's a PySide6 bug. The connection works fine in PyQt6. If want to get it fixed, report it on the [bug tracker](https://bugreports.qt.io/browse/PYSIDE). – ekhumoro Mar 02 '23 at 18:37
  • It's probably related to Shiboken (the library that allows the wrapping of Python around Qt, similarly to `sip` for PyQt). The related code is [here](https://code.qt.io/cgit/pyside/pyside-setup.git/tree/sources/pyside6/libpyside/qobjectconnect.cpp?h=6.3#n223), with some differences between the [PySide2](https://code.qt.io/cgit/pyside/pyside.git/tree/PySide2/QtCore/glue/qobject_connect.cpp#n140) (Qt5) version. I'd suggest you to file a report on the [Qt bug tracker](https://bugreports.qt.io/) using the PYSIDE bug tag. – musicamante Mar 02 '23 at 18:42
  • It's a Pyside thing (see edit). I guess I'll just use pyqt6 and deal with the awful obligatory enum naming. – mahkitah Mar 02 '23 at 19:45

2 Answers2

5

It looks like PySide (or, to be precise, Shiboken, the wrapper that allows Python access to Qt objects) is not able to directly connect to slots of objects directly created by Qt.

That seems a PySide bug: PyQt does not show that behavior, meaning that it's completely possible to achieve it.

A similar behavior sometimes happens with PyQt as well, but that's only in very specific cases: for protected methods of objects directly created by Qt. For example, trying to call initStyleOption() of the default delegate of an item view raises a RuntimeError ("no access to protected functions or signals for objects not created from Python").

Still, that should not happen for public functions like setDefaultSectionSize() is.

There are possible workarounds for that, though.

Using a lambda

As you already found out, you can just use a lambda. This will force PySide to connect to a python function instead of a Qt one: PySide always allows that kind of connection.

The drawback of this approach is the common problem with lambdas: if you directly use it as the connect() argument, you completely lose any reference to it, so there is no way to specifically disconnect from that function, unless you disconnect all functions for that signal (or the whole object).

Lambdas can be referenced to, though:

        header = my_view.verticalHeader()
        header.setDefaultSectionSize_ = lambda s: header.setDefaultSectionSize(s)
        spinbox2.valueChanged.connect(header.setDefaultSectionSize_)

With the code above, you can disconnect the function, since you now have a persistent reference to it.

Using a method

This is similar to the above, with the difference that we create a specific method to handle that, assuming that you keep a reference to the header (or, better, the view):

        spinbox2.valueChanged.connect(self.updateMyViewSectionSize)

    def updateMyViewSectionSize(self, size):
        self.my_view.verticalHeader().setDefaultSectionSize(size)

It might be a bit more verbose, but it's also a better approach, since it consider the dynamic nature of verticalHeader() and provides public access to a function you may need to call in other cases.

Explicitly set the header

This is a trick I normally use for the delegate issue mentioned above: whenever I need to get some info from the delegate based on its initStyleOption(), I just create a new "dummy" delegate; since it has been created in Python, the problem doesn't occur anymore.

The same works for this case too: create and set a dummy header view.

        my_view.setVerticalHeader(QHeaderView(Qt.Vertical, my_view))

Note that both the orientation and arguments are mandatory, and that the above should always be done as soon as possible (right after the table widget has been created).


I would still suggest you to file a report in the Qt bug tracker, hoping they will be able to fix it at least for PySide6 (PySide2 will probably be ignored, but you never know).

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • So, just to see if I understand the problem correctly. my_view.verticalHeader() creates the header in Qt and not in Python, and therefore pyside can't make a direct connection to its slots. Correct? – mahkitah Mar 02 '23 at 21:20
  • No. QTableView (from which QTableHeader inherits) creates its headers as soon as it's created. `verticalHeader()` (similarly to `horizontalHeader()` or `header()` for QTreeView) just returns that object. The difference is that PySide is not able to directly connect to its slot, because it was created on the C++ side. Setting a "custom" QHeaderView can prevent that problem, since it was created in Python. – musicamante Mar 02 '23 at 21:39
  • I just found another way to be able to disconnect from a lambda. You can assign the connection itself: `con1 = spinbox2.valueChanged.connect(lambda x: my_view.verticalHeader().setDefaultSectionSize(x))`. Subsequently you can disconnect by `disconnect(con1)` – mahkitah Mar 18 '23 at 13:44
  • @mahkitah I cannot test it with PySide6, so I am not sure. In recent PyQt5 versions a signal connection returns a QMetaConnection object, while in PySide2 it just returns a bool: if PySide6 hasn't changed the behavior, doing `disconnect(con)` might result in disconnecting *all* connected functions. – musicamante Mar 18 '23 at 15:56
  • I tested the disconnect() on pyqt5 and 6 and pyside2 and 6. It works as it should on all but pyside2. Only the assigned connection is disconnected. On pyside2 you get an error because of the bool. – mahkitah Mar 18 '23 at 21:47
0

Basically, in Qt the meta object system maintains the communication part. Signals, slots and their communication.

To my understanding, the implementation side of pyside looks for the availability of the slot provided, and if not found the above message "You can't add dynamic slots on an object originated from C++." is thrown.

Look for that message in below link.

https://github.com/pyside/PySide/blob/master/PySide/QtCore/glue/qobject_connect.cpp

The first slot in your question controlling splitter may be statically available (while compiling) and the pyside implementation is able to find it. So no error.

The lambda version might have worked because it's anonymous function and pyside implementation might have bypassed searching for it in the list of slots available.

Pavan Chandaka
  • 11,671
  • 5
  • 26
  • 34
  • But that slot (actually, function) *does* exist. This is clearly a bug, probably caused by Shiboken being not able to properly cast the header in its context. A similar behavior happens in PyQt when trying to call protected functions on objects created by Qt (for instance, `initStyleOption()` of the default item delegate), but that should *not* happen for normal public functions. – musicamante Mar 02 '23 at 20:11
  • yes could be.... – Pavan Chandaka Mar 02 '23 at 20:56