Search code examples
pythonpython-3.xpyqtpyqt5qtabwidget

do not change the TAB of the QTabWidget until the form is complete


I am trying to make the user not switch to the next TAB where "Form 2" is located until they fill in Form 1.

I tried the "currentChange" event but it doesn't work the way I want as it shows the alert when it was already changed from TAB.

Is there a way to leave the current TAB fixed until the task is complete?

I attach the code and an image

import sys
from PyQt5.QtCore import Qt
from PyQt5 import QtWidgets

class MyWidget(QtWidgets.QWidget):

    def __init__(self):
        super(MyWidget, self).__init__()
        self.setGeometry(0, 0, 800, 500)
        self.setLayout(QtWidgets.QVBoxLayout())

        #flag to not show the alert when starting the program
        self.flag = True

        #changes to True when the form is completed
        self.form_completed = False

        #WIDGET TAB 1
        self.widget_form1 = QtWidgets.QWidget()
        self.widget_form1.setLayout(QtWidgets.QVBoxLayout())
        self.widget_form1.layout().setAlignment(Qt.AlignHCenter)
        label_form1 = QtWidgets.QLabel("FORM 1")
        self.widget_form1.layout().addWidget(label_form1)

        #WIDGET TAB 2
        self.widget_form2 = QtWidgets.QWidget()
        self.widget_form2.setLayout(QtWidgets.QVBoxLayout())
        self.widget_form2.layout().setAlignment(Qt.AlignHCenter)
        label_form2 = QtWidgets.QLabel("FORM 2")
        self.widget_form2.layout().addWidget(label_form2)

        #QTABWIDGET
        self.tab_widget = QtWidgets.QTabWidget()
        self.tab_widget.currentChanged.connect(self.changed)
        self.tab_widget.addTab(self.widget_form1,"Form 1")
        self.tab_widget.addTab(self.widget_form2, "Form 2")

        self.layout().addWidget(self.tab_widget)

    def changed(self,index):
        if self.flag:
            self.flag = False
            return

        if not self.form_completed:
            QtWidgets.QMessageBox.about(self, "Warning", "You must complete the form")
            return

if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)
    mw = MyWidget()
    mw.show()
    sys.exit(app.exec_())

QTabWidgetProgram


Solution

  • The currentChanged signal is emitted when the index is already changed (the verb is in past tense: Changed), so if you want to prevent the change, you have to detect any user attempt to switch tab.

    In order to do so, you must check both mouse and keyboard events:

    • left mouse clicks on the tab bar;
    • Ctrl+Tab and Ctrl+Shift+Tab on the tab widget;

    Since you have to control that behavior from the main window, the only solution is to install an event filter on both the tab widget and its tabBar(), then if the action would change the index but the form is not completed, you must return True so that the event won't be handled by the widget.

    Please consider that the following assumes that the tab that has to be kept active is the current (the first added tab, or the one set using setCurrentIndex()).

    class MyWidget(QtWidgets.QWidget):
        def __init__(self):
            # ...
            self.tab_widget.installEventFilter(self)
            self.tab_widget.tabBar().installEventFilter(self)
    
        def eventFilter(self, source, event):
            if event.type() == event.KeyPress and \
                event.key() in (Qt.Key_Left, Qt.Key_Right):
                    return not self.form_completed
            elif source == self.tab_widget.tabBar() and \
                event.type() == event.MouseButtonPress and \
                event.button() == Qt.LeftButton:
                    tab = self.tab_widget.tabBar().tabAt(event.pos())
                    if tab >= 0 and tab != self.tab_widget.currentIndex():
                        return self.isInvalid()
            elif source == self.tab_widget and \
                event.type() == event.KeyPress and \
                event.key() in (Qt.Key_Tab, Qt.Key_Backtab) and \
                event.modifiers() & Qt.ControlModifier:
                    return self.isInvalid()
            return super().eventFilter(source, event)
        def isInvalid(self):
            if not self.form_completed:
                QTimer.singleShot(0, lambda: QtWidgets.QMessageBox.about(
                    self, "Warning", "You must complete the form"))
                return True
            return False
    

    Note that I showed the message box using a QTimer in order to properly return the event filter immediately.

    Also consider that it's good practice to connect signals at the end of an object creation and configuration, and this is much more important for signals that notify property changes: you should not connect it before setting the property that could trigger it.
    Since an empty QTabWidget has a -1 index, as soon as you add the first tab the index is changed to 0, thus triggering the signal. Just move the currentChanged signal connection after adding the tabs, and you can get rid of the self.flag check.