Search code examples
pythonqt-designerpyside2

How to connect a signal slot (qt-designer) with a python function


I have created an application with a main window including a MDI area, and a sub window of the MDI area. Both windows are created via QT Designer and saved as ui-file. My python script loads the main window and provides the function to open the sub window. That works so far !

Now I have e.g. a button in the sub window, and it should trigger a function that affects an element in the main window (e.g. show a text in a "PlainTextEdit" element besides the MDI area). In the Qt-Designer I can define the signal and a self-defined slot.

pushButton -> clicked() -> MainWindow -> printText()

My question is: What do I have to write into my python code to catch the signal on the "printText()" slot, to execute a function in the following ?

I'm working with Python 3.7 and Pyside2.

If I run the script the following info are displayed in the terminal:

QObject::connect: No such slot QMainWindow::printText()

QObject::connect: (sender name: 'pushButton')

QObject::connect: (receiver name: 'MainWindow')

  1. The default way via... self.pushButton.clicked.connect(self.function) ... doesn't work, because the pushButton is defined in another class as the main window. (the sub window class) And I also cannot add this code in the sub window class, because with the called function (self.function) I cannot access the element in the main window.

  2. The declaration of the slot (I found so far) in the main window class to catch the signal, doesn't work either:

@Slot()
def printText(self): # name of the slot

    # function which should be executed if the button is clicked
    self.ui.textOutput.setPlainText("This is a test !")

[EDIT] If have added the code of all three files. The example contains 2 subwindows. The first ist included in the main ui-file (always activ by execution). The second subwindow is independed an can be displayed via main menu button.

The py file:

import sys

from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QMainWindow, QWidget, QMdiSubWindow, QMdiArea
from PySide2.QtCore import QFile, Slot, Signal

# Variable which contains the subwindow ID
# Required to determine if a subwindow is already open
state_limitedSubWindow = None

# Main class for loading the UI
class MyUI(QMainWindow):
    def __init__(self, ui_file, parent = None):
        super(MyUI, self).__init__(parent)

        # (1) Open UI file
        ui_file = QFile(ui_file)
        ui_file.open(QFile.ReadOnly)

        # (2) Loading UI file ...
        uiLoader = QUiLoader()
        # ... and creating an instance of the content
        self.ui = uiLoader.load(ui_file)

        # (3) Close file
        ui_file.close()

        # (4) Optional: Customize loaded UI
        # E.g. Set a window title
        self.ui.setWindowTitle("Test")

        # (5) Show the loaded and optionally customized UI
        self.ui.show()

        # A limited subwindow (only on instance can be active)
        self.ui.actionOpenSubWindow.triggered.connect(self.func_limitedSubWindow)

        @Slot()
        def printText():
            print("Debug: Inside the __init__.")

    @Slot()
    def printText(self):
        print("Debug: Inside the MainWindow class")
        self.printing()

    # Limited subwindow via action
    @Slot()
    def func_limitedSubWindow(self):

        # loading global var which contains the subwindow ID
        global state_limitedSubWindow

        if state_limitedSubWindow == None:
            limitedSubWindow = LimitedSubWindow("test_sub.ui")
            self.ui.mdiArea.addSubWindow(limitedSubWindow)
            limitedSubWindow.show()
            # Save ID of the new created subwindow in the global variable
            state_limitedSubWindow = limitedSubWindow.winId()
            # Console output subwindow ID

            print(state_limitedSubWindow)
        else:
            print("Window already exists !")

    @Slot()
    def printing(self):
        self.ui.textOutput.setPlainText("Test")

@Slot()
def printText():
    print("Debug: Outside of the class file")


# Class for the limited second window (only 1 instance can be active)
# This class can of course be in a separate py file
# The base widget of the UI file must be QWidget !!!
class LimitedSubWindow(QWidget):
    def __init__(self, ui_limitedSubWindow_file, parent = None):
        super(LimitedSubWindow, self).__init__(parent)

        # (1) Open UI file
        ui_limitedSubWindow_file = QFile(ui_limitedSubWindow_file)
        ui_limitedSubWindow_file.open(QFile.ReadOnly)

        # (2) Loading UI file ...
        ui_limitedSubWindow_Loader = QUiLoader()
        # ... and creating an instance of the content
        self.ui_limitedSubWindow = ui_limitedSubWindow_Loader.load(ui_limitedSubWindow_file, self)

        # (3) Close file
        ui_limitedSubWindow_file.close()

        self.setMinimumSize(400, 200)

        self.setWindowTitle("Limited subwindow")

        self.ui_limitedSubWindow.pushButton.clicked.connect(self.test)

    # Close event resets the variable which contains the ID
    def closeEvent(self, event):
        global state_limitedSubWindow
        # Reset the global variable
        state_limitedSubWindow = None
        event.accept()


if __name__ == "__main__":

    app = QApplication(sys.argv)

    # Creating an instance of the loading class
    frame = MyUI("test.ui")

    sys.exit(app.exec_())

The main ui-file:

<?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">
   <widget class="QWidget" name="horizontalLayoutWidget">
    <property name="geometry">
     <rect>
      <x>0</x>
      <y>0</y>
      <width>791</width>
      <height>551</height>
     </rect>
    </property>
    <layout class="QHBoxLayout" name="horizontalLayout">
     <item>
      <widget class="QPlainTextEdit" name="textInput"/>
     </item>
     <item>
      <widget class="QMdiArea" name="mdiArea">
       <property name="enabled">
        <bool>true</bool>
       </property>
       <property name="maximumSize">
        <size>
         <width>517</width>
         <height>16777215</height>
        </size>
       </property>
       <widget class="QWidget" name="subwindow">
        <property name="minimumSize">
         <size>
          <width>400</width>
          <height>400</height>
         </size>
        </property>
        <property name="windowTitle">
         <string>Subwindow</string>
        </property>
        <widget class="QPushButton" name="pushButton">
         <property name="geometry">
          <rect>
           <x>160</x>
           <y>200</y>
           <width>90</width>
           <height>28</height>
          </rect>
         </property>
         <property name="text">
          <string>PushButton</string>
         </property>
        </widget>
       </widget>
      </widget>
     </item>
     <item>
      <widget class="QPlainTextEdit" name="textOutput"/>
     </item>
    </layout>
   </widget>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>800</width>
     <height>25</height>
    </rect>
   </property>
   <widget class="QMenu" name="menuWorkbench">
    <property name="title">
     <string>Workbench</string>
    </property>
    <addaction name="actionOpenSubWindow"/>
   </widget>
   <addaction name="menuWorkbench"/>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
  <action name="actionOpenSubWindow">
   <property name="text">
    <string>Caesar Cipher</string>
   </property>
  </action>
  <action name="actionTestText">
   <property name="text">
    <string>Test text</string>
   </property>
  </action>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>pushButton</sender>
   <signal>clicked()</signal>
   <receiver>MainWindow</receiver>
   <slot>printText()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>386</x>
     <y>263</y>
    </hint>
    <hint type="destinationlabel">
     <x>399</x>
     <y>299</y>
    </hint>
   </hints>
  </connection>
 </connections>
 <slots>
  <slot>printText()</slot>
 </slots>
</ui>

The sub ui-file:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Form</class>
 <widget class="QWidget" name="Form">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>315</width>
    <height>242</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <widget class="QPushButton" name="pushButton">
   <property name="geometry">
    <rect>
     <x>90</x>
     <y>80</y>
     <width>90</width>
     <height>28</height>
    </rect>
   </property>
   <property name="text">
    <string>PushButton</string>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>


Solution

  • The printText slot belongs to the MyUI class, but the slot that requires the .ui must belong to the self.ui but unfortunately in PySide2 using QUiLoader is not able to create a class from the .ui.

    So the solution is to convert the .ui to .py using pyside2-uic since it will generate a class.

    Whereas the folder has the following files:

    ├── main.py
    ├── test_sub.ui
    └── test.ui
    

    Then you must open a terminal or CMD located in the project folder and execute:

    pyside2-uic test_sub.ui -o test_sub_ui.py -x
    pyside2-uic test.ui -o test_ui.py -x
    

    So you must obtain the following structure:

    .
    ├── main.py
    ├── test_sub.ui
    ├── test_sub_ui.py
    ├── test.ui
    └── test_ui.py
    

    After that you have to modify the main.py (for more information read this previous answer):

    main.py

    import sys
    from PySide2 import QtCore, QtWidgets
    
    from test_ui import Ui_MainWindow
    from test_sub_ui import Ui_Form
    
    # Variable which contains the subwindow ID
    # Required to determine if a subwindow is already open
    state_limitedSubWindow = None
    
    
    class MyUI(QtWidgets.QMainWindow, Ui_MainWindow):
        def __init__(self, parent=None):
            super(MyUI, self).__init__(parent)
            self.setupUi(self)
            self.setWindowTitle("Test")
            self.mdiArea.addSubWindow(self.subwindow)
            self.actionOpenSubWindow.triggered.connect(self.func_limitedSubWindow)
    
        @QtCore.Slot()
        def printText(self):
            print("Debug: Inside the MainWindow class")
            self.printing()
    
        @QtCore.Slot()
        def func_limitedSubWindow(self):
            # loading global var which contains the subwindow ID
            global state_limitedSubWindow
            if state_limitedSubWindow == None:
                limitedSubWindow = LimitedSubWindow()
                self.mdiArea.addSubWindow(limitedSubWindow)
                limitedSubWindow.show()
                # Save ID of the new created subwindow in the global variable
                state_limitedSubWindow = limitedSubWindow.winId()
                # Console output subwindow ID
                print(state_limitedSubWindow)
            else:
                print("Window already exists !")
            pass
    
        @QtCore.Slot()
        def printing(self):
            self.textOutput.setPlainText("Test")
    
    
    # Class for the limited second window (only 1 instance can be active)
    # This class can of course be in a separate py file
    # The base widget of the UI file must be QWidget !!!
    class LimitedSubWindow(QtWidgets.QWidget, Ui_Form):
        def __init__(self, parent = None):
            super(LimitedSubWindow, self).__init__(parent)
            self.setupUi(self)
            self.setMinimumSize(400, 200)
            self.setWindowTitle("Limited subwindow")
    
        # Close event resets the variable which contains the ID
        def closeEvent(self, event):
            global state_limitedSubWindow
            # Reset the global variable
            state_limitedSubWindow = None
            event.accept()
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        # Creating an instance of the loading class
        frame = MyUI()
        frame.show()
        sys.exit(app.exec_())
    

    So in conclusion QUiLoader has many limitations so it is preferable to use uic.


    If you do not want to use uic then in a previous answer I have indicated how to convert the uic module from PyQt5 to PySide2


    The complete solution can be found here