Search code examples
qt-designerpyside6

PySide6 how to use `QMetaObject.connectSlotsByName(MainWindow)` when using also `QUiLoader().load(...)`


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?


Solution

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