PySide2 - Qt Designer with custom signals and slots of various Python types
-
Hi,
I am trying to develop a Qt application by relying on the Qt Designer and custom PySide2 widgets. The philosophy is that everything that can happen in the Designer should remain there so as to make the Python scripts shorter and more readable (and easier to maintain). I would like to manage all signals and slots connections in the Designer but I run into various problems that includes signal arguments and types. Let me explain further.Expected Workflow
The workflow I envision is the following:- Design the UI as a
QMainWindow
using Qt Designer and native Qt widgets, - If necessary, write custom widgets in Python by subclassing PySide 2 objects binded to native Qt widgets
- Create the custom signals and slots of those custom widgets,
- Promote native Qt widgets in Qt Designer to use the custom widgets
- Write my Python application as a subclass of Pyside2
QMainWindow
, import the UI file created with the Designer (without tweaking the XML manually) and only write a little bit of application logic in there, - Manage all signals and slots connections between the custom widgets and the main window (or application) in Qt Designer.
The Problems & the Questions
While I can add the custom signals and slots in the Designer (according to the link above), I seem to run into two problems:- Some Python types simply don't work with signals:
bool
andint
work, assuming that the custom signal has been defined as such in the Designer butfloat
does not. I first thought that it was because there was a straightforward correspondence between C++ types and Python types but it does not seem to be the case asfloat
does not work. I get an error message such as:
QObject::connect: No such signal CustomButton::buttonClicked(float)
Python data structure types such aslist
andtuple
do not work either. I did not test with more advanced structures such as NumPy arrays for instance. - I cannot pass arguments to the custom slot of a custom widget even if I specify the type (such as a
bool
for instance) in the Designer. I get the following error message:
QObject::connect: No such slot CustomLabel::setCustomText(bool)
Is there a way for me to manage all the custom signals & slots connections directly in the Designer even if said signals carry Python objects and slots receive said Python objects as arguments? I know it is possible to code it in my main application but this creates a ton of boilerplate code as I retrieve all widgets and connect the signals and slots.
Example Code
# test_window.py from PySide2.QtWidgets import QMainWindow from PySide2.QtUiTools import QUiLoader from PySide2.QtCore import QFile # Custom widgets from custom_label import CustomLabel from custom_button import CustomButton class TestWindow(QMainWindow): def __init__(self, path_to_ui_file): super(TestWindow, self).__init__() # Load the UI file self.loadUIFile(path_to_ui_file) self.ui.show() def loadUIFile(self, path_to_UI_file): loader = QUiLoader() loader.registerCustomWidget(CustomLabel) loader.registerCustomWidget(CustomButton) ui_file = QFile(path_to_UI_file) ui_file.open(QFile.ReadOnly) self.ui = loader.load(ui_file, self) ui_file.close() if __name__ == "__main__": import sys from PySide2.QtWidgets import QApplication app = QApplication(sys.argv) _ = TestWindow('test_window.ui') sys.exit(app.exec_())
#custom_button.py from PySide2.QtWidgets import QPushButton from PySide2.QtCore import Signal class CustomButton(QPushButton): buttonClicked = Signal() # Signal() works with no type in the Designer # Signal(bool) works with bool type in the Designer # Signal(int) works with int type in the Designer # Signal(float) does not work # Signal(list) does not with list in the Designer # Signal(tuple) does not with tuple in the Designer def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setText('Press Me!') # Not displayed for some reason def mousePressEvent(self, event): print("Button was clicked") # Just to check the event self.buttonClicked.emit() # Add an argument according to signal type
# custom_label.py from PySide2.QtWidgets import QLabel from PySide2.QtCore import Slot class CustomLabel(QLabel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @Slot() def setCustomText(self): # Add an extra argument here to test self.setText(f"The button was pushed!")
And finally, the UI file produced by the Designer:
test_window.ui
<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MainWindow</class> <widget class="QMainWindow" name="MainWindow"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>800</width> <height>600</height> </rect> </property> <property name="windowTitle"> <string>MainWindow</string> </property> <widget class="QWidget" name="centralwidget"> <layout class="QHBoxLayout" name="horizontalLayout"> <item> <widget class="CustomButton" name="pushButton"> <property name="text"> <string/> </property> </widget> </item> <item> <widget class="CustomLabel" name="label"> <property name="text"> <string/> </property> </widget> </item> </layout> </widget> </widget> <customwidgets> <customwidget> <class>CustomLabel</class> <extends>QLabel</extends> <header location="global">custom_label.h</header> <slots> <slot>setCustomText()</slot> </slots> </customwidget> <customwidget> <class>CustomButton</class> <extends>QPushButton</extends> <header location="global">custom_button.h</header> <slots> <signal>buttonClicked()</signal> </slots> </customwidget> </customwidgets> <resources/> <connections> <connection> <sender>pushButton</sender> <signal>buttonClicked()</signal> <receiver>label</receiver> <slot>setCustomText()</slot> <hints> <hint type="sourcelabel"> <x>202</x> <y>299</y> </hint> <hint type="destinationlabel"> <x>596</x> <y>299</y> </hint> </hints> </connection> </connections> </ui>
- Design the UI as a
-
Thank you @SGaist for leaning in and pointing to this useful wiki page. I better understand how to specify types and overload, both on the signal side and the slot side. Unfortunately, it does not solve the problem that Qt Designer does not handle them.
My example above works perfectly by declaring the signal and slot signatures (for instance
Signal(list)
and@Slot(list)
) and simply adding the following code in my main class__init__()
:self.custom_button = self.ui.findChild(CustomButton, "pushButton") self.custom_label = self.ui.findChild(CustomLabel, "label") self.custom_button.buttonClicked.connect(self.custom_label.setCustomText)
But this is exactly what I want to avoid in my main class as such lines of code quickly grow up with the application complexity and make the code difficult to read.
I define the custom signal and slot in Qt Designer for each custom widget (as mentioned in this post) and then configure the connection using the embedded Signal & Slot Editor (sorry, somebody installed this in French):
This configuration is reflected in the XML file produced by the Designer. See below:<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MainWindow</class> <widget class="QMainWindow" name="MainWindow"> [...] <customwidgets> <customwidget> <class>CustomLabel</class> <extends>QLabel</extends> <header location="global">custom_label.h</header> <slots> <slot>setCustomText(list)</slot> </slots> </customwidget> <customwidget> <class>CustomButton</class> <extends>QPushButton</extends> <header location="global">custom_button.h</header> <slots> <signal>buttonClicked(list)</signal> </slots> </customwidget> </customwidgets> <resources/> <connections> <connection> <sender>pushButton</sender> <signal>buttonClicked(list)</signal> <receiver>label</receiver> <slot>setCustomText(list)</slot> <hints> [...] </connection> </connections> </ui>
Unfortunately, all I get when running my program is the error message below, meaning that the signal is not even recognized properly:
QObject::connect: No such signal CustomButton::buttonClicked(list) QObject::connect: (sender name: 'pushButton') QObject::connect: (receiver name: 'label')
Any idea? Should I go ask this question somewhere dedicated to the Qt Designer? Thanks.
-
Would it be possible for you to provide a minimal project that shows that behaviour ?
I currently don't know how the connection for custom signals and slots from the uic file is handled in Python.
-
Would it be possible for you to provide a minimal project that shows that behaviour ?
I currently don't know how the connection for custom signals and slots from the uic file is handled in Python.
@SGaist Sure thing! There you go! Thank you for your commitment to solving this.
EDIT: I import the UI file directly but converting it to a Python file could indeed give us a hint about what's wrong.
I didpyside2-uic test.ui > test.py
and inspected thetest.py
file. The connection appears properly:... self.pushButton.buttonPressed.connect(self.label.setCustomText) ...
I haven't tested using the generated Python class though. It may work but it breaks a little bit the workflow I envisioned :-/ I wish I could just import the UI file directly without conversion.
-
So from what you wrote, I would say that the loader class has an issue.
-
Thank you @SGaist,
I have updated my example repository with the two ways to use UI files: direct import and Python conversion. Long story short: direct import fails but Python conversion works!Any idea if I could report the
QUiLoader
issue somewhere ? -
Sure: the bug report system
Don't forget to post back the link here, it will make the ticket easier to find :-)
-
Sure: the bug report system
Don't forget to post back the link here, it will make the ticket easier to find :-)