Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

[PySide2] Connecting a signal to setValue does not work when Loading UI through QUiLoader



  • Hi, I am facing the following problem: I am loading a UI designed with Qt Designer in PySide and now want to attach some signals to the ui to interact with the data model. Therefore I created a Signal that I am connecting with a setValue function of a QDoubleSpinBox. But everytime I emit some data into the signal, it does not react and the value of the QDoubleSpinBox. In the search for a cause for this unclear behavior, I also created another QDoubleSpinBox manually and also connected the signal to it. Here the value gets changed. The example code is here:

    import sys
    import time
    from PySide2 import QtGui, QtCore, QtWidgets
    
    import pyqtgraph as pg
    import numpy as np
    
    from PySide2.QtCore import *
    from PySide2.QtUiTools import QUiLoader
    from PySide2.QtWidgets import QMainWindow, QDoubleSpinBox
    
    
    class PressureViewModel(QObject):
        _current_pressure = 0.0
        _current_pressure_sig = Signal(object)
    
        def __init__(self,parent):
            QtCore.QObject.__init__(self)
            ui_file = QFile("scratch.ui")
            ui_file.open(QFile.ReadOnly)
            loader = QUiLoader()
            window = loader.load(ui_file,parent)
            self.sb = QDoubleSpinBox(parent)
            window.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.sb)
            self.ui = window
    
    
    
        def initialize_signals(self):
            self._current_pressure_sig.connect(self.ui.current_pressure_spin_box.setValue) #does not seem to work
            self._current_pressure_sig.connect(self.sb.setValue) #works
    
    
        @property
        def current_pressure(self):
            return self._current_pressure
    
        @current_pressure.setter
        def current_pressure(self ,value):
            print("Setter called")
            self._current_pressure_sig.emit(value)
            self._current_pressure = value
    
    
    class PressureController:
        def __init__(self, view_model:PressureViewModel):
            self.view_model = view_model
    
        def cycle_controller(self):
            for i in range(20):
                print(self.view_model.current_pressure)
                time.sleep(0.1)
                self.view_model.current_pressure = np.random.normal(loc=10,scale=2)
            return 0.0
    
    
    
    class Worker(QThread):
    
    
        def __init__(self, controller:PressureController):
            QThread.__init__(self)
            self.controller = controller
    
        def run(self):
            while True:
                self.controller.cycle_controller()
    
    
    app = QtGui.QApplication([])
    
    window = QMainWindow()
    pVM = PressureViewModel(window)
    pC = PressureController(pVM)
    
    window.setCentralWidget(pVM.ui)
    pVM.initialize_signals()
    window.show()
    
    worker = Worker(pC)
    worker.start()
    
    sys.exit(app.exec_())
    

    The ui file is the following:

    <?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>400</width>
        <height>300</height>
       </rect>
      </property>
      <property name="windowTitle">
       <string>Form</string>
      </property>
      <layout class="QFormLayout" name="formLayout">
       <item row="0" column="0">
        <widget class="QLabel" name="current_pressure_label">
         <property name="text">
          <string>Current Pressure</string>
         </property>
        </widget>
       </item>
       <item row="0" column="1">
        <widget class="QDoubleSpinBox" name="current_pressure_spin_box"/>
       </item>
      </layout>
     </widget>
     <resources/>
     <connections/>
    </ui>
    

    I have the feeling that I somehow "loose connection" to the proper ui elements and am really not connecting the signal to the correct function. Does anybody has an idea where my error could be?



  • Hi @tyde,

    I think your issue may be to do with threading. Generally speaking, it's better not to sub-class QThread, but to move objects into a QThread. Also, I think you are making direct calls between threads when you need to use the Signal/Slot mechanism to communicate.

    The following example uses your UI file and groups the functionality together a little. The PressureController is now passed in to a QThread owned by the QMainWindow that also constructs the UI. Everything is connected together via signals and slots.

    import time
    
    from PySide2.QtCore import QFile, QObject, qApp, QThread, Signal, Slot, Property
    from PySide2.QtWidgets import QMainWindow
    from PySide2.QtUiTools import QUiLoader
    import numpy as np
    
    
    class PressureController(QObject):
        currentPressure = Signal(float)
        stopped = Signal()
    
        def __init__(self):
            super().__init__()
            self._looping = False
    
        @Slot()
        def run(self):
            self._looping = True
            try:
                while self._looping:
                    self.currentPressure.emit(np.random.normal(loc=10, scale=2))
                    qApp.processEvents()
                    time.sleep(0.1)
            except Exception as e:
                print(e)
    
            self.stopped.emit()
    
        @Slot()
        def stop(self):
            self._looping = False
    
    
    class PressureViewModel(QMainWindow):
        stopThread = Signal()
    
        def __init__(self, parent=None, **kwargs):
            super().__init__(parent, **kwargs)
    
            self._currentPressure = 0.0
    
            ui_file = QFile("./scratch.ui")
            ui_file.open(QFile.ReadOnly)
            ui_loader = QUiLoader()
            self.ui = ui_loader.load(ui_file, parent)
            self.setCentralWidget(self.ui)
    
            self.pressureController = PressureController()
            self.pressureController.currentPressure.connect(self.setCurrentPressure)
            self.thread = QThread()
            self.thread.started.connect(self.pressureController.run)
            self.pressureController.stopped.connect(self.thread.quit)
            self.pressureController.moveToThread(self.thread)
            self.stopThread.connect(self.pressureController.stop)
            self.thread.start()
    
        def readCurrentPressure(self):
            return self._currentPressure
    
        @Slot(float)
        def setCurrentPressure(self, pressure):
            if self._currentPressure == pressure:
                return
            self._currentPressure = pressure
    
            self.ui.current_pressure_spin_box.setValue(pressure)
    
        currentPressure = Property(float, readCurrentPressure, setCurrentPressure)
    
        def closeEvent(self, event):
            # This stops the thread when the window is closed, otherwise the program 
            # technically crashes because the thread it orphaned.
            if self.thread.isRunning():
                self.stopThread.emit()
    
                while not self.thread.isFinished():
                    qApp.processEvents()
    
            event.accept()
    
    
    if __name__ == "__main__":
        from sys import argv, exit
        from PySide2.QtCore import QCoreApplication, Qt
        from PySide2.QtWidgets import QApplication
    
        QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
        a = QApplication(argv)
        pvm = PressureViewModel()
        pvm.show()
        exit(a.exec_())
    
    

    Please feel free to ask questions about anything that I haven't explained.

    Hope this helps :o)



  • Hi @tyde,

    I think your issue may be to do with threading. Generally speaking, it's better not to sub-class QThread, but to move objects into a QThread. Also, I think you are making direct calls between threads when you need to use the Signal/Slot mechanism to communicate.

    The following example uses your UI file and groups the functionality together a little. The PressureController is now passed in to a QThread owned by the QMainWindow that also constructs the UI. Everything is connected together via signals and slots.

    import time
    
    from PySide2.QtCore import QFile, QObject, qApp, QThread, Signal, Slot, Property
    from PySide2.QtWidgets import QMainWindow
    from PySide2.QtUiTools import QUiLoader
    import numpy as np
    
    
    class PressureController(QObject):
        currentPressure = Signal(float)
        stopped = Signal()
    
        def __init__(self):
            super().__init__()
            self._looping = False
    
        @Slot()
        def run(self):
            self._looping = True
            try:
                while self._looping:
                    self.currentPressure.emit(np.random.normal(loc=10, scale=2))
                    qApp.processEvents()
                    time.sleep(0.1)
            except Exception as e:
                print(e)
    
            self.stopped.emit()
    
        @Slot()
        def stop(self):
            self._looping = False
    
    
    class PressureViewModel(QMainWindow):
        stopThread = Signal()
    
        def __init__(self, parent=None, **kwargs):
            super().__init__(parent, **kwargs)
    
            self._currentPressure = 0.0
    
            ui_file = QFile("./scratch.ui")
            ui_file.open(QFile.ReadOnly)
            ui_loader = QUiLoader()
            self.ui = ui_loader.load(ui_file, parent)
            self.setCentralWidget(self.ui)
    
            self.pressureController = PressureController()
            self.pressureController.currentPressure.connect(self.setCurrentPressure)
            self.thread = QThread()
            self.thread.started.connect(self.pressureController.run)
            self.pressureController.stopped.connect(self.thread.quit)
            self.pressureController.moveToThread(self.thread)
            self.stopThread.connect(self.pressureController.stop)
            self.thread.start()
    
        def readCurrentPressure(self):
            return self._currentPressure
    
        @Slot(float)
        def setCurrentPressure(self, pressure):
            if self._currentPressure == pressure:
                return
            self._currentPressure = pressure
    
            self.ui.current_pressure_spin_box.setValue(pressure)
    
        currentPressure = Property(float, readCurrentPressure, setCurrentPressure)
    
        def closeEvent(self, event):
            # This stops the thread when the window is closed, otherwise the program 
            # technically crashes because the thread it orphaned.
            if self.thread.isRunning():
                self.stopThread.emit()
    
                while not self.thread.isFinished():
                    qApp.processEvents()
    
            event.accept()
    
    
    if __name__ == "__main__":
        from sys import argv, exit
        from PySide2.QtCore import QCoreApplication, Qt
        from PySide2.QtWidgets import QApplication
    
        QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
        a = QApplication(argv)
        pvm = PressureViewModel()
        pvm.show()
        exit(a.exec_())
    
    

    Please feel free to ask questions about anything that I haven't explained.

    Hope this helps :o)


  • Banned

    Note @tyde first be very careful about using super( ) in python as it requires that you understand 4 other coding issues that it creates when using it as well as the current known bugs it has. Conversely doing it explicitly (as you had done) does not have any issues nor bugs.

    Next if you do not need to use a UI from the Designer because you are not supporting numerous code implementations then I would strongly suggest you learn to render your GUI using the standard method of implementation which if done right actually creates proper and simple code as opposed to the translated UI (assuming you are translating it to python-qt) or the less standard XML version if you are using it non-translated. I suggest this because if you actually learn to code Python-Qt as it was meant to be coded you can do it just as quickly or quicker than using the Designer and the code you end up with is easier to understand and facilitates Python-Qt interactivity rather than hampering it by requiring you to implement specialized coding to make your "black-box code" (which is something that is inherently dangerous btw) work nicely with the rest of your code.

    Also you should not sub-class a QThread in Python-Qt5 as that is not how it is implemented anymore you instead push your code into the QThread as @jazzycamel has shown -- I have a simpler MUC (Minimal Usable Code) example for this as well if you need/want it.

    If you need help with creating a standard Python-Qt Gui I have MUC examples that outline various ways to implement such quickly and easily


Log in to reply