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?
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.