'pyqt5 - update a dynamically created widget with its corresponding data

I have created a tool with help of a custom widget.

custom widget

This widget fits into the main window main window like so. User can create new instances of custom widget with a click of a add widget button.

enter image description here

custom widget :

class RenderWidget(QWidget):
    def __init__(self, toolname, process_queue):
        QWidget.__init__(self)
        self.initSubject()
        self.organize()
        self.toolname = toolname
        self.process_queue = process_queue

    def initSubject(self):
        self.script_dir = os.path.dirname(os.path.realpath(__file__))
        self.scene_widget = uic.loadUi(os.path.join(self.script_dir, 'ui', 'test_widget.ui'))
        self.scene_widget.pushButton_scenefile.clicked.connect(lambda x: self.open_file_name_dialog(x))

    def organize(self):
        grid = QGridLayout(self)
        self.setLayout(grid)
        grid.addWidget(self.scene_widget)

    def open_file_name_dialog(self, x):
        dialog = QFileDialog(
            self,
            'submit_render',
            "path",
            "*.ma",
            # supportedSchemes=["file"],
            options=QFileDialog.DontUseNativeDialog,
        )
        self.change_button_name(dialog)
        dialog.findChild(QTreeView).selectionModel().currentChanged.connect(
            lambda: self.change_button_name(dialog)
        )
        if dialog.exec_() == QDialog.Accepted:
            filename = dialog.selectedUrls()[0]
            filename = filename.toLocalFile()
            self.scene_widget.lineEdit_scenefile.setText(filename)
            if validate_path(self.toolname, filename):
                self.process_queue.put((filename, _count))

    def change_button_name(self, dialog):
        for btn in dialog.findChildren(QPushButton):
            if btn.text() == self.tr("&Open"):
                QTimer.singleShot(0, lambda btn=btn: btn.setText("open"))

Main Widget :

class MyAppView(QWidget):
    def __init__(self, toolname, process_queue):
        self.toolname = toolname
        self.process_queue = process_queue

        super(MyAppView, self).__init__()
        self.initUi()

    def initUi(self):
        self.layoutV = QVBoxLayout(self)

        self.area = QScrollArea(self)
        self.area.setWidgetResizable(True)
        self.scrollAreaWidgetContents = QWidget()
        self.scrollAreaWidgetContents.setGeometry(0, 0, 200, 100)

        self.layoutH = QHBoxLayout(self.scrollAreaWidgetContents)
        self.layoutH_Button = QHBoxLayout(self.scrollAreaWidgetContents)
        self.gridLayout = QGridLayout()
        self.layoutH.addLayout(self.gridLayout)

        self.area.setWidget(self.scrollAreaWidgetContents)
        self.left_spacer = QSpacerItem(50, 10, QSizePolicy.Minimum)
        self.center_spacer = QSpacerItem(100, 10, QSizePolicy.Minimum)
        self.submit_button = UVPush("Submit")
        self.cancel_button = UVPush("Cancel")

        # Log text
        self.output = QPlainTextEdit()
        self.output.setFixedHeight(150)
        self.output.setPlaceholderText('output..')
        self.layoutH_Button.addSpacerItem(self.left_spacer)
        self.layoutH_Button.addWidget(self.submit_button)
        self.layoutH_Button.addSpacerItem(self.center_spacer)
        self.layoutH_Button.addWidget(self.cancel_button)

        self.layoutV.addWidget(self.area)
        self.layoutV.addLayout(self.layoutH_Button)
        self.layoutV.addWidget(self.output)

        self.widget = RenderWidget(self.toolname, self.process_queue)
        self.gridLayout.addWidget(self.widget)
        self.setGeometry(700, 200, 350, 300)


Process :

class ChildProc(Process):
    def __init__(self, to_emitter, from_mother, daemon=True):
        super(ChildProc, self).__init__()
        self.daemon = daemon
        self.to_emitter = to_emitter
        self.data_from_mother = from_mother
        self.maya_output = MayaOutput()

    def run(self):
        from python_library.maya_python import data_from_maya_scene
        while True:
            filename, count = self.data_from_mother.get()
            print('+++ filename ', filename, count)
            _cameras, _frames, _layers = data_from_maya_scene(filename)
            self.maya_output.cameras = _cameras
            self.maya_output.layers = _layers
            self.maya_output.frames = _frames
            self.maya_output.count = count
            self.to_emitter.send(self.maya_output)
            #TODO call signal slot to start stop loader

(ref: How to signal slots in a GUI from a different process?)

Emitter :

class Emitter(QThread):
    ui_data_available = pyqtSignal(object)  # Signal indicating new UI data is available.

    def __init__(self, from_process):
        super(Emitter, self).__init__()
        self.data_from_process = from_process

    def run(self):
        while True:
            try:
                _object = self.data_from_process.recv()
            except EOFError:
                break
            else:
                self.ui_data_available.emit(_object)

MainWindow :

class CloudRenderView(QtWidgets.QMainWindow):
    def __init__(self, toolname, child_process_queue, emitter):
        QtWidgets.QMainWindow.__init__(self)
        self.toolname = toolname
        self.process_queue = child_process_queue
        self.MyAppView = MyAppView(self.toolname, self.process_queue)

        .
        .
        self.emitter = emitter
        self.emitter.daemon = True
        self.emitter.start()

        # set window dimension
        self.connections()

        .
        .

    def connections(self):
        .
        .
        self.emitter.ui_data_available.connect(self.update_ui)

    .
    .


    def update_ui(self, maya_object):
        count = maya_object.count
        example_widget = self.MyAppView.gridLayout.itemAt(count).widget()

        if 'Error' not in maya_object.cameras:
            example_widget.scene_widget.textEdit_camera.setText('\n'.join([str(elem) for elem in maya_object.cameras]))
        else:
            example_widget.scene_widget.textEdit_camera.setHtml("<font color='red' size='2'><red>{}</font>".format('Error'))

        if 'Error' not in maya_object.frames:
            example_widget.scene_widget.lineEdit_frames.setText(maya_object.frames)
        else:
            example_widget.scene_widget.lineEdit_frames.setText('Error')

        if 'Error' not in maya_object.layers:
            example_widget.scene_widget.textEdit_render_layer.setText('\n'.join([str(elem) for elem in maya_object.layers]))
        else:
            example_widget.scene_widget.textEdit_render_layer.setHtml("<font color='red' size='2'><red>{}</font>".format('Error'))


Question : A button choose file to choose file from system is clicked. This selected file is send to a qprocess via queue and after processing is complete, emitter sends signal to main UI via a pipe.

Coming to the question, say data was entered for widget 3 or 4, After process, data gets send back to widget no 1 , always.

How to map it to same widget that it was selected from. ?



Solution 1:[1]

Attempting an answer that will help but without code and knowing your particular approach it's difficult.

I'm assuming that you are not sending the data to the correct widget because your reference to that widget is wrong. You need a way to reference the new widget that you create, and there are generally two ways to do this.

  1. Store a reference to the new widget in a place you can easily get to it, like a self._second_widget variable or a list or dictionary of variables. Then when you send data to your widget, use that reference.

  2. Find the widget in the QGridLayout using appropriate calls, which could be something like .itemAtPosition(x,y).widget()

Hopefully this helps - if you want a better answer it's a good idea to post some code.

Solution 2:[2]

My code certainly has a lot of scope to be optimized. I see that. But in context of what is required. I.e. get index of custom widget, answer from @eyllanesc's helped. @eyllanesc, You rock.

installEventFilter in PyQt5

In, class RenderWidget

class RenderWidget(QWidget):
    clicked = pyqtSignal(int)
    def __init__(self, toolname, process_queue, index):
        QWidget.__init__(self)
        self.index = index                              <--------
        self.initSubject()
        self.process_queue = process_queue

    def initSubject(self):
        .
        .
        self.scene_widget = uic.loadUi(os.path.join(self.script_dir, 'ui', 'scene_widget.ui'))
        self.scene_widget.setProperty("index", self.index)   <--------
        .
        self.scene_widget.installEventFilter(self)

    def eventFilter(self, widget, event):          <--------
        if isinstance(widget, QWidget) and event.type() == QEvent.MouseButtonPress:
            i = widget.property("index")           <--------
            self.clicked.emit(i)                   <--------
        return QWidget.eventFilter(self, widget, event)

def open_file_name_dialog(self, x):
    .
    dialog = QFileDialog(
        self,
        'submit_render',
        "path",
        "*.ma",
        # supportedSchemes=["file"],
        options=QFileDialog.DontUseNativeDialog,
    )
    if dialog.exec_() == QDialog.Accepted:
        filename = dialog.selectedUrls()[0]
        filename = filename.toLocalFile()
        self.scene_widget.lineEdit_scenefile.setText(filename)

        if validate_path(self.toolname, filename):
            widget_count = self.index - 1                        <--------
            self.process_queue.put((filename, widget_count))     <--------
       .

After accepting input from user, before passing data to qprocess get index value.

Finally, in QMainWindow where user clicks "+ icon" button, increment index and pass the value

class CloudRenderView(QtWidgets.QMainWindow):
    def __init__(self, toolname, child_process_queue, emitter):
        QtWidgets.QMainWindow.__init__(self)
        self.toolname = toolname
        self.process_queue = child_process_queue
        self.MyAppView = MyAppView(self.toolname, self.process_queue)
        self.widget_count = 0                          <--------

def add_maya_widget(self):
    self.widget_count += 1                        <--------
    self.widget = RenderWidget(self.toolname, self.process_queue, self.widget_count)                                    <--------

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Alex
Solution 2 nish