Search code examples
pythonpyqt5

PyQt5: changing current index of QStackedWidget from widget class in separate module?


I am developing a PyQt app with 8 pages. On each page, the user should be able to seemlessly navigate to the other 7 pages that exists. "Seemless" meaning there should be no flash when switching, and the new screen opens in the same window. Being new to this, I followed an example on YouTube that seemed good. It showed using a QStackedWidget. Here is some example code of the method of switching screens:

def gotologin(self):
    login = LoginScreen()
    widget.addWidget(login)
    widget.setCurrentIndex(widget.currentIndex() + 1)

def gotocreate(self):
    create = CreateAccScreen()
    widget.addWidget(create)
    widget.setCurrentIndex(widget.currentIndex() + 1)

And at first this worked because all classes were in the same module, and the main method at the bottom looked like this:

#main
app = QApplication(sys.argv)
welcome = WelcomeScreen()
widget = QStackedWidget()
widget.addWidget(welcome)
widget.setFixedHeight(800)
widget.setFixedWidth(1200)
widget.show()
try:
    sys.exit(app.exec_())
except:
    print("Exiting")

However, as that file gets bigger, I decided to put the classes for later pages into their own files. They are in a separate file with no main method, so they cannot reference "widget".

I can pass a reference to the "widget" object to the constructors of the later pages...but something tells me this isn't the best idea. How could I change the current widget from each and every class? Or would a different approach to app structure be preferable?

EDIT: minimum reproducible example below:

import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QDialog, QApplication, QStackedWidget

class Page1Screen(QDialog):
    def __init__(self):
        super(Page1Screen, self).__init__()
        self.setupUi(self)

        self.treeWidget.itemClicked.connect(self.switch_screens)

    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(571, 508)
        self.label = QtWidgets.QLabel(Dialog)
        self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
        self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
        self.label.setObjectName("label")
        self.treeWidget = QtWidgets.QTreeWidget(Dialog)
        self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
        self.treeWidget.setObjectName("treeWidget")
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
        self.label.setText(_translate("Dialog", "Page 1"))
        self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
        __sortingEnabled = self.treeWidget.isSortingEnabled()
        self.treeWidget.setSortingEnabled(False)
        self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
        self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
        self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
        self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
        self.treeWidget.setSortingEnabled(__sortingEnabled)

    def switch_screens(self):
        item = self.treeWidget.currentItem()
        name = item.text(0)
        if name == "PAGE 1":
            print("We're already on this page")
        elif name == "PAGE 2":
            p2 = Page2Screen()
            widget.addWidget(p2)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 3":
            p3 = Page3Screen()
            widget.addWidget(p3)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 4":
            p4 = Page4Screen()
            widget.addWidget(p4)
            widget.setCurrentIndex(widget.currentIndex() + 1)

class Page2Screen(QDialog):
    def __init__(self):
        super(Page2Screen, self).__init__()
        self.setupUi(self)

        self.treeWidget.itemClicked.connect(self.switch_screens)

    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(571, 508)
        self.label = QtWidgets.QLabel(Dialog)
        self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
        self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
        self.label.setObjectName("label")
        self.treeWidget = QtWidgets.QTreeWidget(Dialog)
        self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
        self.treeWidget.setObjectName("treeWidget")
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        self.label_2 = QtWidgets.QLabel(Dialog)
        self.label_2.setGeometry(QtCore.QRect(0, 0, 571, 511))
        self.label_2.setStyleSheet("background: qlineargradient(spread:pad, x1:0.989, y1:1, x2:0.073, y2:0.119, stop:0 rgba(176, 121, 254, 255), stop:1 rgba(255, 255, 255, 255));")
        self.label_2.setText("")
        self.label_2.setObjectName("label_2")
        self.label_2.raise_()
        self.label.raise_()
        self.treeWidget.raise_()

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
        self.label.setText(_translate("Dialog", "Page 2"))
        self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
        __sortingEnabled = self.treeWidget.isSortingEnabled()
        self.treeWidget.setSortingEnabled(False)
        self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
        self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
        self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
        self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
        self.treeWidget.setSortingEnabled(__sortingEnabled)

    def switch_screens(self):
        item = self.treeWidget.currentItem()
        name = item.text(0)
        if name == "PAGE 1":
            p1 = Page1Screen()
            widget.addWidget(p1)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 2":
            print("We're already on this page")
        elif name == "PAGE 3":
            p3 = Page3Screen()
            widget.addWidget(p3)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 4":
            p4 = Page4Screen()
            widget.addWidget(p4)
            widget.setCurrentIndex(widget.currentIndex() + 1)

class Page3Screen(QDialog):
    def __init__(self):
        super(Page3Screen, self).__init__()
        self.setupUi(self)

        self.treeWidget.itemClicked.connect(self.switch_screens)

    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(571, 508)
        self.label = QtWidgets.QLabel(Dialog)
        self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
        self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
        self.label.setObjectName("label")
        self.treeWidget = QtWidgets.QTreeWidget(Dialog)
        self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
        self.treeWidget.setObjectName("treeWidget")
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        self.label_2 = QtWidgets.QLabel(Dialog)
        self.label_2.setGeometry(QtCore.QRect(0, 0, 571, 511))
        self.label_2.setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.339795 rgba(255, 0, 0, 255), stop:0.339799 rgba(255, 255, 255, 255), stop:0.662444 rgba(255, 255, 255, 255), stop:0.662469 rgba(0, 0, 255, 255), stop:1 rgba(0, 0, 255, 255));")
        self.label_2.setText("")
        self.label_2.setObjectName("label_2")
        self.label_2.raise_()
        self.label.raise_()
        self.treeWidget.raise_()

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
        self.label.setText(_translate("Dialog", "Page 3"))
        self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
        __sortingEnabled = self.treeWidget.isSortingEnabled()
        self.treeWidget.setSortingEnabled(False)
        self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
        self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
        self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
        self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
        self.treeWidget.setSortingEnabled(__sortingEnabled)

    def switch_screens(self):
        item = self.treeWidget.currentItem()
        name = item.text(0)
        if name == "PAGE 1":
            p1 = Page1Screen()
            widget.addWidget(p1)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 2":
            p2 = Page2Screen()
            widget.addWidget(p2)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 3":
            print("We're already on this page")
        elif name == "PAGE 4":
            p4 = Page4Screen()
            widget.addWidget(p4)
            widget.setCurrentIndex(widget.currentIndex() + 1)

class Page4Screen(QDialog):
    def __init__(self):
        super(Page4Screen, self).__init__()
        self.setupUi(self)

        self.treeWidget.itemClicked.connect(self.switch_screens)

    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(571, 508)
        self.label = QtWidgets.QLabel(Dialog)
        self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
        self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
        self.label.setObjectName("label")
        self.treeWidget = QtWidgets.QTreeWidget(Dialog)
        self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
        self.treeWidget.setObjectName("treeWidget")
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
        self.label_2 = QtWidgets.QLabel(Dialog)
        self.label_2.setGeometry(QtCore.QRect(0, 0, 571, 511))
        self.label_2.setStyleSheet("background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0.132, y2:0.153682, stop:0 rgba(58, 169, 255, 255), stop:1 rgba(255, 255, 255, 255));")
        self.label_2.setText("")
        self.label_2.setObjectName("label_2")
        self.label_2.raise_()
        self.label.raise_()
        self.treeWidget.raise_()

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
        self.label.setText(_translate("Dialog", "Page 4"))
        self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
        __sortingEnabled = self.treeWidget.isSortingEnabled()
        self.treeWidget.setSortingEnabled(False)
        self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
        self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
        self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
        self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
        self.treeWidget.setSortingEnabled(__sortingEnabled)

    def switch_screens(self):
        item = self.treeWidget.currentItem()
        name = item.text(0)
        if name == "PAGE 1":
            p1 = Page1Screen()
            widget.addWidget(p1)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 2":
            p2 = Page2Screen()
            widget.addWidget(p2)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 3":
            p3 = Page3Screen()
            widget.addWidget(p3)
            widget.setCurrentIndex(widget.currentIndex() + 1)
        elif name == "PAGE 4":
            print("We're already on this page")

#main
app = QApplication(sys.argv)
welcome = Page1Screen()
widget = QStackedWidget()
widget.addWidget(welcome)
widget.setFixedHeight(571)
widget.setFixedWidth(508)
widget.show()
try:
    sys.exit(app.exec_())
except:
    print("Exiting")

Solution

  • In your example, each time you switch to a new page it creates a sizeable memory leak that keeps expanding. You can see this for yourself using tools like tracemalloc to track the memory footprint your app is using.

    For example at the top of your code near the imports add,

    import tracemalloc
    tracemalloc.start()
    

    And then at the end of each of your PageScreen class __init__ constructors add print(tracemalloc.get_traced_memory()), then launch your code and move start switching pages back and forth and you will see the memory footprint grow and grow with every page change.

    This is because each time you switch to a new page you are constructing a new instance of your PageScreen without destroying the previous copy. It would be much more efficient and stable to simply define all of your various pages once and add them to the stackedWidget in the very first top level widget you construct while and and then adding it and the tree widget which you are using to change pages to the top level window. This has the added benefit of reducing repetitive code as well.

    For example, below I create a widget that behaves similarly to your example, accept it using all of the information I already stated. I am also using QWidget instead of QDialog simply because the Qt docs describe QDialog as:

    "A dialog window is a top-level window mostly used for short-term tasks and brief communications with the user."

    Also since all of your PageScreen widgets are identical other than their stylesheet, I am only going to define one QWidget to represent all of them and then pass the stylesheet in as a parameter.

    import sys
    from PyQt5 import QtWidgets, QtCore
    from PyQt5.QtWidgets import QApplication, QStackedWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel
    import tracemalloc
    
    class Window(QWidget):
        stylesheets = [   # you can store your stylesheets in a list to easily reuse
            "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.339795 rgba(255, 0, 0, 255), stop:0.339799 rgba(255, 255, 255, 255), stop:0.662444 rgba(255, 255, 255, 255), stop:0.662469 rgba(0, 0, 255, 255), stop:1 rgba(0, 0, 255, 255));",
            "background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0.132, y2:0.153682, stop:0 rgba(58, 169, 255, 255), stop:1 rgba(255, 255, 255, 255));",
            "background: qlineargradient(spread:pad, x1:0.989, y1:1, x2:0.073, y2:0.119, stop:0 rgba(176, 121, 254, 255), stop:1 rgba(255, 255, 255, 255));",
            "font: 75 12pt \"MS Shell Dlg 2\";"
        ]
    
        def __init__(self, parent=None) -> None:
            super().__init__(parent=parent)
            layout = QHBoxLayout(self)   # use layout managers
            self.treeWidget = QtWidgets.QTreeWidget(self)
            # The tree widget is created only once in the top level window
            self.stacked = QStackedWidget()   # same thing for the stacked widget
            self.widgets = []
            self.treeWidget.setHeaderLabel("New Column")
            self.treeWidget.currentItemChanged.connect(self.switch_screens)
            for i in range(1, 9):
                # create each of the tree items and each of the stack pages
                # only once for each page.
                QtWidgets.QTreeWidgetItem(self.treeWidget, [f"PAGE {i}"], 0)
                self.widgets.append(
                    PageScreen(self.stylesheets[i % len(self.stylesheets)])
                )
                self.stacked.addWidget(self.widgets[-1])  # add each page to the stacked widget
            self.resize(571,508)
            layout.addWidget(self.treeWidget)    # add the tree and the stacked widget
            layout.addWidget(self.stacked)       # to the layout manager
    
        def switch_screens(self):
            # Here I am using the the tree items text to identify which page to switch
            # to, however there are countless ways to achieve the same thing.
            item = self.treeWidget.currentItem()
            name = item.text(0)   # name = Page 1
            index = int(name.split()[1])  # index = 1
            self.stacked.setCurrentIndex(index)
            print(tracemalloc.get_traced_memory())
    
    class PageScreen(QWidget):
        def __init__(self, stylesheet):
            # passing the stylesheet in as a parameter allows to reduce a bunch of duplicate code.
            super().__init__()
            self.setStyleSheet(stylesheet)
            self.layout = QVBoxLayout(self)  # use a layout manager
            self.label = QLabel("Label")
            self.layout.addWidget(self.label)
    
    if __name__ == "__main__":
        tracemalloc.start()
        app = QApplication([])
        window = Window()
        window.show()
        print(tracemalloc.get_traced_memory())
        app.exec()
    

    If you do what I suggested above with tracemalloc module in your code, and then run my example, which also prints out memory snapshots you will easily be able to see a difference once you start switching pages.

    I know this doesn't really answer your question directly, but using this strategy there is no need to switch the page from a different module or class, and it eliminates any need for global variables as well.

    Finally if for some reason you did want each page of the stacked widget to have it's own copy of the treeWidget all you would need to do is define the treeWidget inside the PageScreen class and instead of switching the page directly you would emit a signal from the PageScreen that get's received by the Top level window which then makes triggers the stacked widget to switch pages, so there would still be no need for a global variable.

    That configuration might look something like this.

    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    
    class Window(QStackedWidget):
        stylesheets = [   # you can store your stylesheets in a list to easily reuse
            "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.339795 rgba(255, 0, 0, 255), stop:0.339799 rgba(255, 255, 255, 255), stop:0.662444 rgba(255, 255, 255, 255), stop:0.662469 rgba(0, 0, 255, 255), stop:1 rgba(0, 0, 255, 255));",
            "background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0.132, y2:0.153682, stop:0 rgba(58, 169, 255, 255), stop:1 rgba(255, 255, 255, 255));",
            "background: qlineargradient(spread:pad, x1:0.989, y1:1, x2:0.073, y2:0.119, stop:0 rgba(176, 121, 254, 255), stop:1 rgba(255, 255, 255, 255));",
            "font:  \"MS Shell Dlg 2\";"
        ]
    
        def __init__(self, parent=None) -> None:
            super().__init__(parent=parent)
            self.widgets = {}
            for i in range(9):
                page = PageScreen(self.stylesheets[i % len(self.stylesheets)])
                self.widgets[i] = page
                self.addWidget(page)
                page.pageSelected.connect(self.switch_page)
            self.resize(571,508)
    
        def switch_page(self, index):
            self.setCurrentWidget(self.widgets[index])
            self.widgets[index].select_tree_item(index)
    
    
    class PageScreen(QWidget):
    
        pageSelected = pyqtSignal(int)
    
        def __init__(self, stylesheet):
            super().__init__()
            self.setStyleSheet(stylesheet)
            self.layout = QHBoxLayout(self)
            self.label = QLabel("Label")
            policy = self.label.sizePolicy()
            policy.setHorizontalPolicy(policy.Policy.MinimumExpanding)
            self.label.setSizePolicy(policy)
            self.treeWidget = QTreeWidget(self)
            self.treeWidget.setHeaderLabel("New Column")
            for i in range(1, 9):
                QTreeWidgetItem(self.treeWidget, [f"PAGE {i}"], 0)
            self.treeWidget.itemClicked.connect(self.switch_screens)
            self.layout.addWidget(self.treeWidget)
            self.layout.addWidget(self.label)
    
        def select_tree_item(self, index):
            for i in range(self.treeWidget.topLevelItemCount()):
                if self.treeWidget.topLevelItem(i).text(0).endswith(str(index)):
                    self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(i))
                    break
    
        def switch_screens(self, item, column):
            text = item.text(column)
            index = int(text.split()[1])
            self.pageSelected.emit(index)
    
    
    if __name__ == "__main__":
        app = QApplication([])
        window = Window()
        window.show()
        app.exec()
    

    Hope this helps.