Search code examples
pythonpyqtpyqt5qstackedwidget

Dynamically modifying multiple instances of the same widget via QStackedWidget


I am unable to dynamically change the layout/elements of multiple instances of a QStackedWidget. The full code below enables one to toggle a button which when clicked will create a QFrame with the respective name of the button that was toggled and when untoggled will therefore remove the frame with the respective name associated with the button. However I wish to be able to use the QComboBox right next to it to enable one to modify the layout of QFrames which have been created via the buttons on the left. In previous examples I have been able to unclick all the buttons, therefore deleting the frames, then selecting the correct layout via the combobox and then seeing the adjusted layout for the frame when I recreate the frame by retoggling the button on. I am looking for a solution that will allow me to obviously do this without having to click and unclick the frame's respective buttons.

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys



class Box(QFrame):
    def __init__ (self,title):
        super().__init__()
        self.title = title

        self.frame1_UI = QWidget()
        self.frame2_UI = QWidget()
        self.frame3_UI= QWidget()

        self.frame1()
        self.frame2()
        self.frame3()

        self.Stack = QStackedWidget(self)
        self.Stack.addWidget(self.frame1_UI)
        self.Stack.addWidget(self.frame2_UI)
        self.Stack.addWidget(self.frame3_UI)

        name = QLabel(title)  
        task_h_box = QHBoxLayout()
        task_h_box.addWidget(name)
        task_h_box.addWidget(self.Stack)
        self.setLayout(task_h_box)

    def text(self):
        return self.title

    def frame1(self):
        label = QLabel('frame1')
        task_h_box = QHBoxLayout()
        task_h_box.addWidget(label)
        self.setStyleSheet("QFrame{background-color: rgb(0,0,0); border-radius: 10px;}")
        self.frame1_UI.setLayout(task_h_box)
    
    def frame2(self):
        
        label = QLabel('frame2')
        task_h_box = QHBoxLayout()
        task_h_box.addWidget(label)
        self.frame2_UI.setLayout(task_h_box)
        self.setStyleSheet("QFrame{background-color: rgb(100,100,100); border-radius: 10px;}")

    def frame3(self):
        label = QLabel('frame3')
        task_h_box = QHBoxLayout()
        task_h_box.addWidget(label)
        self.frame3_UI.setLayout(task_h_box)
        self.setStyleSheet("QFrame{background-color: rgb(150,150,150); border-radius: 10px;}")


class window(QMainWindow):
    def __init__(self):
        super().__init__()
        

        widget = QWidget(self)
        
        #Widgets
        creator_button = QPushButton('Creator')
        creator_button.setCheckable(True)
        creator_button.clicked.connect(self.creation)

        creator_button2 = QPushButton('Creator 2')
        creator_button2.setCheckable(True)
        creator_button2.clicked.connect(self.creation)
        
        self.control_button = QComboBox()
        self.control_button.addItem('frame1')
        self.control_button.addItem('frame2')
        self.control_button.addItem('frame3')
        
        self.control_button.currentIndexChanged.connect(self.transition)
        
        #Layouts
        self.layout = QHBoxLayout(widget)
        self.creator_layout = QVBoxLayout()
        self.control_layout = QVBoxLayout()
        self.box_layout = QVBoxLayout()
        self.layout.addLayout(self.creator_layout)
        self.layout.addLayout(self.control_layout)
        self.layout.addLayout(self.box_layout)
        
        #Placements
        self.creator_layout.addWidget(creator_button)
        self.creator_layout.addWidget(creator_button2)
        self.control_layout.addWidget(self.control_button)

        self.setCentralWidget(widget)
        self.boxList = []
        self.show()

    def creation(self, checked):
        sender = self.sender()
        title = str('I am son of ' + sender.text())
        if checked:
            widget = Box(title)
            self.box_layout.addWidget(widget)
            self.boxList.append(widget.text())
        elif not checked:
            x = self.boxList.index(title)
            self.box_layout.itemAt(x).widget().deleteLater()
            self.boxList.remove(title)

    
    def transition(self, i): 
        title = ('I am son of Creator 2') 
        self.Box = Box(title)
        self.Box.Stack.setCurrentIndex(i)

        
        

app = QApplication(sys.argv)
window = window()
sys.exit(app.exec_())

Solution

  • You don't need to create another Box in transition, just change the QStackWidget's index in the existing ones.

    def transition(self, i): 
        for box in self.findChildren(Box):
            box.Stack.setCurrentIndex(i)
    

    It would make sense then to set the index upon creation of the Box too.

    def creation(self, checked):
        sender = self.sender()
        title = str('I am son of ' + sender.text())
        if checked:
            widget = Box(title)
            widget.Stack.setCurrentIndex(self.control_button.currentIndex())
            self.box_layout.addWidget(widget)
            self.boxList.append(widget.text())
        elif not checked:
            x = self.boxList.index(title)
            self.box_layout.itemAt(x).widget().deleteLater()
            self.boxList.remove(title)

    And that should do it with the minimal change to the code. However, the whole thing can be simplified if you show / hide the Box widgets when the buttons are toggled instead of creating / destroying a new instance each time.

    class window(QMainWindow):
        def __init__(self):
            super().__init__()
            widget = QWidget(self)
    
            self.box = Box('I am son of Creator', visible=False)
            self.box2 = Box('I am son of Creator 2', visible=False)
    
            creator_button = QPushButton('Creator', checkable=True)
            creator_button.toggled[bool].connect(self.box.setVisible)
            creator_button2 = QPushButton('Creator 2', checkable=True)
            creator_button2.toggled[bool].connect(self.box2.setVisible)
            
            control_button = QComboBox()
            control_button.addItems(['frame1', 'frame2', 'frame3'])
            control_button.currentIndexChanged.connect(self.transition)
            
            grid = QGridLayout(widget)
            grid.addWidget(creator_button, 0, 0)
            grid.addWidget(creator_button2, 1, 0)
            grid.addWidget(control_button, 0, 1, 2, 1)
            grid.addWidget(self.box, 0, 2)
            grid.addWidget(self.box2, 1, 2)
            
            self.setCentralWidget(widget)
            self.show()
        
        def transition(self, i):
            self.box.Stack.setCurrentIndex(i)
            self.box2.Stack.setCurrentIndex(i)
    

    And just need to add unpack keyword arguments to Box constructor for visible=False.

    class Box(QFrame):
        def __init__ (self, title, **kwargs):
            super().__init__(**kwargs)