0

I'm trying to make a button that would be universal for each window in the program that would result in the previous window being opened regardless of the window it is currently in.

I've been unable to think of proper logic to do this without the occurence of a circular import in my program.

Could someone suggest any way to implement this feature?

Koshy John
  • 11
  • 4
  • If you plan to do some sort of "wizard", then consider using QStackedWidget as container for those "windows" (which are actually "pages"), and add that to a window that also contains buttons to switch. Also consider using QWizard. – musicamante Dec 30 '22 at 23:55
  • its not exactly but just a matter of choice. if the user isnt happy with a certain product, he can go back and check the others. – Koshy John Jan 01 '23 at 17:25

1 Answers1

0

When writing my own applications, I use the concept of Scenes (such as in the Unity framework):

  • Each scene is an asset that must be available at all times, and should not be released from memory;
  • Each scene defines what's being currently rendered at the given time;
  • Only one scene can be active at a time;
  • If a new scene is set as current, the old one's contents must be properly released from memory.

This way, there's no need to open/close different windows for each different interface. There's also no need to destroy them: we can simply unparent an old scene, and set the new scene as the current scene to prevent it from being deleted by the Qt API.

Using this approach, we can store each scene's references since their creation inside some type of data structure (in this case a dictionary), and set them as current whenever we see fit.

This example shows this kind of scene logic I use at the moment, though anyone can use it if you want to. This example shows the kind of logic you're asking for:

from PySide2.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
from PySide2.QtWidgets import QLabel, QPushButton, QWidgetItem

# A modified version of what's written here:
# https://stackoverflow.com/questions/9374063/remove-all-items-from-a-layout/9383780#9383780
def clearLayout(layout):
    if (layout is not None):
        while (True):
            child = layout.takeAt(0)

            if (child is not None):
                if (isinstance(child, QWidgetItem)):
                    widget = child.widget()
                    if (widget is not None):
                        widget.close()
                        widget.deleteLater()
                elif (not isinstance(child, QSpacerItem)):
                    clearLayout(child.layout())
            else:
                break

# Create our base scene class:
class Scene(QWidget):
    def __init__(self, mwin):
        QWidget.__init__(self)
        self.setLayout(QVBoxLayout())
        self.window = mwin

    # Virtual method. Should be overwritten by subclasses.
    def start(self):
        pass

    def finish(self):
        clearLayout(self.layout())

# Crate your custom scenes:
class Scene1(Scene):
    def __init__(self, mwin):
        Scene.__init__(self, mwin)

    def start(self):
        layout = self.layout()

        backbt = QPushButton('Back To Scene 3')
        nextbt = QPushButton('Foward To Scene 2')

        # Assign Scene1 Logic
        backbt.clicked.connect(lambda: self.window.setScene('scene3'))
        nextbt.clicked.connect(lambda: self.window.setScene('scene2'))

        layout.addWidget(QLabel('Scene 1'))
        layout.addWidget(backbt)
        layout.addWidget(nextbt)

class Scene2(Scene):
    def __init__(self, mwin):
        Scene.__init__(self, mwin)

    def start(self):
        layout = self.layout()

        backbt = QPushButton('Back To Scene 1')
        nextbt = QPushButton('Foward To Scene 3')

        # Assign Scene2 Logic
        backbt.clicked.connect(lambda: self.window.setScene('scene1'))
        nextbt.clicked.connect(lambda: self.window.setScene('scene3'))

        layout.addWidget(QLabel('Scene 2'))
        layout.addWidget(backbt)
        layout.addWidget(nextbt)

class Scene3(Scene):
    def __init__(self, mwin):
        Scene.__init__(self, mwin)

    def start(self):
        layout = self.layout()

        backbt = QPushButton('Back To Scene 2')
        nextbt = QPushButton('Foward To Scene 1')

        # Assign Scene3 Logic
        backbt.clicked.connect(lambda: self.window.setScene('scene2'))
        nextbt.clicked.connect(lambda: self.window.setScene('scene1'))

        layout.addWidget(QLabel('Scene 3'))
        layout.addWidget(backbt)
        layout.addWidget(nextbt)

        print('Scene3: ', len(self.findChildren(QWidget)))

class MainWindow(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self)
        self.currScene = None

        # Assign scenes to the main window:
        # 1. prevent garbage collection
        # 2. allows us to retrieve them from any other
        #    scene using only a string key, given we
        #    pass the MainWindow reference to each scene.
        # 3. All the imports should go into this module
        #    alone. All other scenes do not need to import
        #    each other modules. They just need to use the
        #    MainWindow.setScene method with the right key.
        self.scenes = {}
        self.scenes['scene1'] = Scene1(self)
        self.scenes['scene2'] = Scene2(self)
        self.scenes['scene3'] = Scene3(self)

        # Start with scene1
        self.setScene('scene1')

    def setScene(self, name):
        # Releases the old scene, hides it and unparents it
        # so it can be used again.
        if (self.currScene is not None):
            self.currScene.finish()
            self.currScene.hide()

            # unparent to take back ownership of the widget
            self.currScene.setParent(None)

        # Set the current reference.
        self.currScene = self.scenes.get(name)

        # Sets the new scene as current, start them, and
        # display them at the screen.
        if (self.currScene is not None):
            self.setCentralWidget(self.currScene)
            self.currScene.start()
            self.currScene.show()

if __name__ == '__main__':
    app = QApplication()
    win = MainWindow()
    win.show()
    app.exec_()

About the circular references. By using this approach, you could redefine every class here into separate modules. This way you would avoid using circular references:

  • Scene1.py imports
    • Scene.py
  • Scene2.py imports
    • Scene.py
  • Scene3.py imports
    • Scene.py
  • MainWindow.py imports
    • Scene1.py
    • Scene2.py
    • Scene3.py
  • main.py imports
    • MainWindow.py
Carl HR
  • 776
  • 5
  • 12
  • Why the whole clearing/recreating the layout? Also, reparenting the widgets doesn't seem very useful for this, and all this can be more easily done with QStackedWidget. – musicamante Dec 30 '22 at 23:15
  • The whole clearing/recreating the layout is useful to conserve memory. I know that modern hardware can support lots GB of data on RAM.. but still I find it wrong to rely on that. So everything that I'm not using at the moment, I release it from memory. Also, yes, the whole thing can be done using a QStackedWidget. But as wrote on the answer: *this is how I do it*. I wrote this script when I just started using PySide2. So I didn't know about the QStackedWidget at the time, and I just copy/paste it everywhere since then. But yes, it has the same functionality as the scenes are never destroyed. – Carl HR Dec 31 '22 at 00:02
  • @musicamante now, if the goal is to increase performance, yes, clearing/recreating the layout is bad (if the page contains too many widgets, for example). In this case, sacrificing RAM to save performance could be a good thing. It really depends on the situation. – Carl HR Dec 31 '22 at 00:07
  • But you're not releasing anything: you're just removing widgets from the layout, but they still exist in memory, as `QLayout.takeAt()` only removes layout items but doesn't change ownership. In fact, you're actually making it worse, as every time `start()` is called, *more* widgets are added (just add a `print(len(self.findChildren(QWidget)))` at the end of that function and you'll see). I understand that that code "worked" for you at the time, but you should also consider that as your experience increases, you should always revise what you wrote. Sorry, but that code has *lots* of issues. – musicamante Dec 31 '22 at 00:22
  • @musicamante Ok thanks for pointing that out. I tried using the `print(len(self.findChildren(QWidget)))` as you suggested and yes, you're right. I thought that `QWidget.close()` automatically called `QObject.deleteLater()`. Reading it again from the docs, well, it doesn't. By adding a `deleteLater` call right after `QWidget.close()` inside the finish method, it should fix that issue right? – Carl HR Dec 31 '22 at 00:48
  • @musicamante When you say *Sorry, but that code has lots of issues*, you mean there's more? Or are you referring to the fact that I should use a QStackedWidget? If so, point that out. I'm always open for suggestions. – Carl HR Dec 31 '22 at 00:55
  • Yes, `close()` does not (nor should!) delete the widget, unless `WA_DeleteOnClose` was set, `deleteLater()` is the only way to "safely" doing it and (possibly) removing it from memory (even if that doesn't ensure that memory is *actually* released, especially for Python). Keep in mind, though, that widgets don't occupy that much RAM: unless you have a program with widget count in the order of *several thousands*, the UI rarely requires more than 10-15mb even for extremely complex cases. A normal widget normally takes no more than 5-10kb, with complex ones rarely being above 50-70kb. – musicamante Dec 31 '22 at 01:24
  • 1
    Considering that Qt6 is already requiring systems that rarely work fine on their own under 2gB of RAM, and systems with less RAM are probably too old anyway to properly run a Qt5 program, the eventual benefit provided by deleting/restoring widgets would be rather irrelevant, especially if compared to the level of complexity (and increased possibility of problems, just like it happened with you). Not to mention the fact that windows probably have input widgets that is important to keep (consider a wizard). In fact, widgets should be deleted only when it makes sense, not as default behavior. – musicamante Dec 31 '22 at 01:24
  • 1. `window` is an existing (and quite useful) function of all QWidgets, and shouldn't be overwritten; 2. you probably wanted to use recursion, but `clearLayout()` is not in your code; 3. it shouldn't be responsibility of a child to interact with its parent (see separation of concerns), that's one of the reasons for which signals exist; 4. you don't check if the new scene exists before hiding the other; 5. (as per your update) `child` is QLayoutItem, which isn't a QObject, so there's `deleteLater()` function; QLayout will destroy it, you have to call it on the item's `widget()` or `layout()`. – musicamante Dec 31 '22 at 01:26
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250758/discussion-between-carl-hr-and-musicamante). – Carl HR Dec 31 '22 at 01:28