Are stacked widgets a good way to manage view navigation

I built a QWidget to integrate Jupyter Lab into my project and wanted to get your impressions and advice. I have main.py that shows the JupyterWidget in a single iframe, and main2,py that passes a callback and an extra parameter to allow navigation. I want to add additional widgets to launch from the main widget and navigate between them.

jupyter_widget.py

from PyQt6.QtCore import QUrl, QTimer
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel
from PyQt6.QtWebEngineWidgets import QWebEngineView

class JupyterWidget(QWidget):
    def __init__(self, switch_to_main_callback=None, extra_param=None):
        super().__init__()

        self.browser = QWebEngineView()

        # Add a startup page
        self.browser.setHtml('''
            <!DOCTYPE html>
            <html>
            <head>
                <title>Welcome</title>
            </head>
            <body>
                <h1>Welcome to the Jupyter Notebook Interface</h1>
                <p>Checking Jupyter Server status...</p>
            </body>
            </html>
        ''')

        # Button to go back to main menu if callback provided
        if switch_to_main_callback:
            self.back_button = QPushButton('Back to Main Menu')
            self.back_button.clicked.connect(switch_to_main_callback)
        else:
            self.back_button = None

        # Create layout for the widget
        layout = QVBoxLayout()

        # Add back button if it exists
        if self.back_button:
            layout.addWidget(self.back_button)
        
        # Add the web view
        layout.addWidget(self.browser)

        # Extra param usage example, add only if not None or empty
        if extra_param:
            self.extra_label = QLabel(f"Extra Param: {extra_param}")
            layout.addWidget(self.extra_label)

        self.setLayout(layout)

        self.jupyter_process = None
        self.check_server_and_start()

    def check_server_and_start(self):
        if not self.is_server_running():
            self.start_jupyter()
        else:
            self.load_jupyter_lab()

    def is_server_running(self):
        import socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        result = sock.connect_ex(('localhost', 8888))
        return result == 0

    def start_jupyter(self):
        import subprocess
        self.jupyter_process = subprocess.Popen(['jupyter', 'lab', '--no-browser'])
        QTimer.singleShot(2000, self.load_jupyter_lab)  # Adjust delay as needed

    def load_jupyter_lab(self):
        self.browser.setUrl(QUrl("http://localhost:8888/lab"))

    def stop_jupyter(self):
        if self.jupyter_process:
            self.jupyter_process.terminate()
            self.jupyter_process = None

    def closeEvent(self, event):
        self.stop_jupyter()  # Ensure the server is stopped
        event.accept()

main.py


import subprocess
import socket
import jupyter_widget

from PyQt6.QtCore import QUrl, QTimer
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QVBoxLayout
from PyQt6.QtWebEngineWidgets import QWebEngineView
import sys
import subprocess
import socket
from jupyter_widget import JupyterWidget

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Frank Math')
        self.resize(1600, 900)

        jupyter_widget = JupyterWidget()
        self.setCentralWidget(jupyter_widget)
     
    def closeEvent(self, event):
        self.centralWidget().stop_jupyter()  # Ensure the server is stopped
        event.accept()   
        
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

main2.py


from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QStackedWidget
import sys
from jupyter_widget import JupyterWidget

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Frank Math Navigator')
        self.resize(800, 600)

        # Create stacked widget
        self.stacked_widget = QStackedWidget()

        # Create main menu layout
        self.main_menu = QWidget()
        self.main_layout = QVBoxLayout()

        # Add buttons for different programs
        self.jupyter_button = QPushButton('Open Jupyter Notebook')
        self.jupyter_button.clicked.connect(self.show_jupyter)

        self.main_layout.addWidget(self.jupyter_button)
        self.main_menu.setLayout(self.main_layout)

        # Add widgets to stacked widget
        self.stacked_widget.addWidget(self.main_menu)

        self.jupyter_widget = None

        self.setCentralWidget(self.stacked_widget)

    def show_jupyter(self):
        if self.jupyter_widget is None:
            self.jupyter_widget = JupyterWidget(self.show_main_menu, extra_param="Extra Info")
            self.stacked_widget.addWidget(self.jupyter_widget)
        self.stacked_widget.setCurrentWidget(self.jupyter_widget)

    def show_main_menu(self):
        if self.jupyter_widget:
            self.jupyter_widget.stop_jupyter()
            self.jupyter_widget = None
        self.stacked_widget.setCurrentWidget(self.main_menu)

    def closeEvent(self, event):
        if self.jupyter_widget:
            self.jupyter_widget.stop_jupyter()
        event.accept()

app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

I haven’t used the stacked widget but I thought I’d mention how I went about handling dynamic layout management.

I had problems with the Wayland window system under Ubuntu and found I was unable to center any window on my screen. So I went in another direction by redeveloping my application as an SDI (Single Document Interface) where the main screen was maximized and I handled navigation to different pages via context buttons appearing as needed.

I developed a clear_section method in my application that would clear all widgets and sublayouts from the main screen which I used for navigation and screen displays. I designated various layouts as root layouts which persist for the application (self.???)

This means more code to write but it does provide flexabilities to dynamically navigate to different screens with different buttons at different times.

def clear_section(self, section):

if section == “all”:
self.current_layout = self.base_layout
layout = self.base_layout
else:
self.current_layout = section
layout = section

for widget_no in range(0, layout.count()):
if “layout” in str(layout.itemAt(widget_no)):
self.previous_layout = layout
self.clear_section(layout.itemAt(widget_no))
else
layout.itemAt(widget_no).widget().deleteLater()

if self.current_layout != self.base_layout:
if self.current_layout != self.buttons_section:
self.current_layout.deleteLater()

if self.previous_layout is not None:
self.current_layout = self.previous_layout
self.previous_layout = None

Sorry my code wouldn’t format properly…