Reputation: 3004
I have the following test code:
from os import path
from PySide6.QtCore import QObject, QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication
class MyWin(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = QUiLoader().load(path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
def show(self):
self.ui.show()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.show()
app.exec()
with its associated MainWindow.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="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>19</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
... which works as expected.
Question is: how do I replace the line:
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
with an equivalent using QMetaObject.connectSlotsByName(???)
?
Problem here is PySide6 QUiLoader
is incapable to add widgets as children of self
(as PyQt6 uic.loadUi(filename, self)
can do) and thus I'm forced to put UI in a separate variable (self.ui
) while slots are defined in "parent" MyWin
.
How can I circumvent limitation?
Reason why I ask is my real program has zillions of signals/slots and connect()
'ing them manually is a real PITA (and error-prone)
UPDATE:
Following advice I modified MyWin
to inherit from QWidget
, but enabling self.ui.setParent(self)
is enough to prevent display of UI.
from os import path
from PySide6.QtCore import QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget
class MyWin(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = QUiLoader().load(path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
self.ui.setParent(self)
# QMetaObject.connectSlotsByName(self)
def myshow(self):
self.ui.show()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.myshow()
app.exec()
I also see some strange errors:
mcon@ikea:~/projects/pyside6-test$ venv/bin/python t.py
qt.pysideplugin: Environment variable PYSIDE_DESIGNER_PLUGINS is not set, bailing out.
qt.pysideplugin: No instance of QPyDesignerCustomWidgetCollection was found.
Qt WebEngine seems to be initialized from a plugin. Please set Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute and QSGRendererInterface::OpenGLRhi using QQuickWindow::setGraphicsApi before constructing QGuiApplication.
^C^C^C^C
Terminated
I need to kill process from another terminal, normal Ctrl-C is ignored.
UPDATE2: I further updated code following @ekhumoro advice:
from os import path
from PySide6.QtCore import QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget, QMainWindow
class UiLoader(QUiLoader):
_baseinstance = None
def createWidget(self, classname, parent=None, name=''):
if parent is None and self._baseinstance is not None:
widget = self._baseinstance
else:
widget = super(UiLoader, self).createWidget(classname, parent, name)
if self._baseinstance is not None:
setattr(self._baseinstance, name, widget)
return widget
def loadUi(self, uifile, baseinstance=None):
self._baseinstance = baseinstance
widget = self.load(uifile)
QMetaObject.connectSlotsByName(widget)
return widget
class MyWin(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
UiLoader().loadUi(path.join(path.dirname(__file__), "MainWindow.ui"), self)
# self.pushButton.clicked.connect(self.on_pushButton_clicked)
QMetaObject.connectSlotsByName(self)
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.show()
app.exec()
This doesn't work either: it shows GUI, but button click is not connected (unless I explicitly do it uncommenting the line).
What am I doing wrong?
Upvotes: 2
Views: 965
Reputation: 120618
To answer the question as stated in the title:
It's possible to fix the original example by setting the container-widget as the parent of the ui-widget. However, there are a few extra steps required. Firstly, the flags of the ui-widget must include Qt.Window, otherwise it will just become the child of an invisble window. Secondly, the close-event of the ui-widget must be reimplemented so that the application shuts down properly. And finally, the auto-connected slots must be decorated with QtCore.Slot
.
Here's a fully working example:
from os import path
from PySide6.QtCore import Qt, QEvent, Slot, QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget
class MyWin(QWidget):
def __init__(self):
super().__init__()
self.ui = QUiLoader().load(
path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.setParent(self, self.ui.windowFlags() | Qt.WindowType.Window)
self.ui.installEventFilter(self)
QMetaObject.connectSlotsByName(self)
def eventFilter(self, source, event):
if event.type() == QEvent.Type.Close and source is self.ui:
QApplication.instance().quit()
return super().eventFilter(source, event)
def myshow(self):
self.ui.show()
@Slot()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication(['Test'])
win = MyWin()
win.myshow()
app.exec()
PS: see also my completely revised alternative solution using a loadUi
-style approach that now works properly with both PySide2 and PySide6.
Upvotes: 1
Reputation: 48335
connectSlotsByName()
is a static function, and it can only accept an argument (the "target" object), meaning that it can only operate with children of the object and its own functions.
The solution, then, is to make the top level widget a child of the "controller".
This cannot be directly done with setParent()
in your case, though, since the QWidget override of setParent()
expects a QWidget as argument, and your MyWin
class is a simple QObject instead.
While the theoretical solution could be to call QObject.setParent(self.ui, self)
to circumvent the override, this won't work and will sooner or later crash. As explained in this related post, the only available solution is to make the "controller" a QWidget subclass, even if you're not showing it.
Note that there is an alternative solution that should provide the same results as uic.loadUi
using a QUiLoader subclass, as explained in this answer. It obviously misses the other PyQt parameters, but for general usage it shouldn't be a problem.
Finally, remember that you could always use the loadUiType
function that works exactly like the PyQt one (again, without extra parameters); it's a slightly different pattern, since it generates the class names dynamically, but has the benefit that it parses the ui just once, instead of doing it every time a new instance is created.
With a custom function you can even create a class constructor with the path and return a type that also calls setupUi()
on its own:
def UiClass(path):
formClass, widgetClass = loadUiType(path)
name = os.path.basename(path).replace('.', '_')
def __init__(self, parent=None):
widgetClass.__init__(self, parent)
formClass.__init__(self)
self.setupUi(self)
return type(name, (widgetClass, formClass), {'__init__': __init__})
class Win(UiClass('mainWindow.ui')):
def __init__(self):
super().__init__()
# no need to call setupUi()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
test = Win()
test.show()
sys.exit(app.exec())
With the above, print(Win.__mro__)
will show the following (this is from PyQt, PySide will obviously have the appropriate module name):
(<class '__main__.Win'>, <class '__main__.mainWindow_ui'>,
<class 'PyQt5.QtWidgets.QMainWindow'>, <class 'PyQt5.QtWidgets.QWidget'>,
<class 'PyQt5.QtCore.QObject'>, <class 'sip.wrapper'>,
<class 'PyQt5.QtGui.QPaintDevice'>, <class 'sip.simplewrapper'>,
<class 'Ui_MainWindow'>, <class 'object'>)
As noted in the comment by @ekhumoro, since PySide6 the behavior has changed, since uic
is now able to directly output python code, so you need to ensure that the uic
command of Qt is in the user PATH
.
PS: note that, with PyQt, using connectSlotsByName()
always calls the target function as many overrides as the signal has, which is the case of clicked
signal of buttons; this is one of the few cases for which the @pyqtSlot
decorator is required, so in your case you should decorate the function with @pyqtSlot()
, since you are not interested in the checked
argument. For PySide, instead, the @Slot
is mandatory in order to make connectSlotsByName()
work.
See this related answer.
Upvotes: 1