Intercept QTabWidget tab change [duplicate] - tabs

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_())

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.

Related

when there are two inputs, how to get the function input, instead of the input from the main body

Here is my code, it is copyrighted cited here
Al Sweigart. Invent Your Own Computer Games with Python. August 28, 2015. https://inventwithpython.com/invent4thed/chapter5.html. Accessed December 4, 2020.
#import modules
import random
import time
#def functions
def intro():
guess = int(input("There are two caves, in one is magic treasure and in the other is a
terrible monster. Hit 1 or 2 to guess. "))
return guess
def check_cave(guess):
print("the cave is dank")
time.sleep(2)
print("you hear a sound")
time.sleep(2)
print("a monster jumps out and opens its jaws and...")
time.sleep(2)
friendly_cave = random.randint(1,2)
if str(guess) == str(friendly_cave):
print("leaves so you can take the treasure, congratulations!")
else:
print("kills you bahahaha")
print(guess)
#main body of code
play_again = 'no'
guess = int(input("please enter a guess of one or two "))
while play_again == 'yes' or 'y':
intro()
check_cave(guess)
play_again = input("want to play again? yes or no...")
if play_again == 'yes':
continue
else:
break
I expect the code to ask the user for input from the main body of the code, then ask for same input from inside a function later (the function returns the input), I expect the function's returned value to overwrite the original input. but it doesnt. Instead it keeps the original input from outside the function what can I do about this?
You must assign value returned from function intro() to a variable:
while play_again == 'yes' or 'y':
guess = intro() # modified this line
check_cave(guess)
play_again = input("want to play again? yes or no...")
if play_again == 'yes':
continue
else:
break

QTabWidget Access Actual Tab (not the content widget)

In this image:
I would like to access the actual tabs, rather than the content, so I can set a QPropertyAnimation on the actual tab when it is hovered on. I know how to get the hover event working, and I can get the tab index on the hover, I just can't access the actual tab when I hover on it. Is there a list of the tabs somewhere as an attribute of the QTabBar or the QTabWidget, or where can I find the tabs? Or do I have to subclass the addTab function to create the tabs individually?
Extra Info
Using PyQt5.14.1
Windows 10
Python 3.8.0
You cannot access "tabs", as they are not objects, but an abstract representation of the contents of the tab bar list.
The only way to customize their appearance is by subclassing QTabBar and overriding the paintEvent().
In order to add an over effect, you have to provide a unique animation for each tab, so you have to keep track of all tabs that are inserted or removed. The addTab, insertTab and removeTab methods are not valid options, since they are not used by QTabWidget. It uses instead tabInserted() and tabRemoved(), so those are to be overridden too.
This could be a problem with stylesheets, though, especially if you want to set fonts or margins.
Luckily, we can use the qproperty-* declaration with custom PyQt properties, and in the following example I'm using them for the tab colors.
class AnimatedTabBar(QtWidgets.QTabBar):
def __init__(self, *args):
super().__init__(*args)
palette = self.palette()
self._normalColor = palette.color(palette.Dark)
self._hoverColor = palette.color(palette.Mid)
self._selectedColor = palette.color(palette.Light)
self.animations = []
self.lastHoverTab = -1
#QtCore.pyqtProperty(QtGui.QColor)
def normalColor(self):
return self._normalColor
#normalColor.setter
def normalColor(self, color):
self._normalColor = color
for ani in self.animations:
ani.setEndValue(color)
#QtCore.pyqtProperty(QtGui.QColor)
def hoverColor(self):
return self._hoverColor
#hoverColor.setter
def hoverColor(self, color):
self._hoverColor = color
for ani in self.animations:
ani.setStartValue(color)
#QtCore.pyqtProperty(QtGui.QColor)
def selectedColor(self):
return self._selectedColor
#selectedColor.setter
def selectedColor(self, color):
self._selectedColor = color
self.update()
def tabInserted(self, index):
super().tabInserted(index)
ani = QtCore.QVariantAnimation()
ani.setStartValue(self.normalColor)
ani.setEndValue(self.hoverColor)
ani.setDuration(150)
ani.valueChanged.connect(self.update)
self.animations.insert(index, ani)
def tabRemoved(self, index):
super().tabRemoved(index)
ani = self.animations.pop(index)
ani.stop()
ani.deleteLater()
def event(self, event):
if event.type() == QtCore.QEvent.HoverMove:
tab = self.tabAt(event.pos())
if tab != self.lastHoverTab:
if self.lastHoverTab >= 0:
lastAni = self.animations[self.lastHoverTab]
lastAni.setDirection(lastAni.Backward)
lastAni.start()
if tab >= 0:
ani = self.animations[tab]
ani.setDirection(ani.Forward)
ani.start()
self.lastHoverTab = tab
elif event.type() == QtCore.QEvent.Leave:
if self.lastHoverTab >= 0:
lastAni = self.animations[self.lastHoverTab]
lastAni.setDirection(lastAni.Backward)
lastAni.start()
self.lastHoverTab = -1
return super().event(event)
def paintEvent(self, event):
selected = self.currentIndex()
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
style = self.style()
fullTabRect = QtCore.QRect()
tabList = []
for i in range(self.count()):
tab = QtWidgets.QStyleOptionTab()
self.initStyleOption(tab, i)
tabRect = self.tabRect(i)
fullTabRect |= tabRect
if i == selected:
# make the selected tab slightly bigger, but ensure that it's
# still within the tab bar rectangle if it's the first or the last
tabRect.adjust(
-2 if i else 0, 0,
2 if i < self.count() - 1 else 0, 1)
pen = QtCore.Qt.lightGray
brush = self._selectedColor
else:
tabRect.adjust(1, 1, -1, 1)
pen = QtCore.Qt.NoPen
brush = self.animations[i].currentValue()
tabList.append((tab, tabRect, pen, brush))
# move the selected tab to the end, so that it can be painted "over"
if selected >= 0:
tabList.append(tabList.pop(selected))
# ensure that we don't paint over the tab base
margin = max(2, style.pixelMetric(style.PM_TabBarBaseHeight))
qp.setClipRect(fullTabRect.adjusted(0, 0, 0, -margin))
for tab, tabRect, pen, brush in tabList:
qp.setPen(pen)
qp.setBrush(brush)
qp.drawRoundedRect(tabRect, 4, 4)
style.drawControl(style.CE_TabBarTabLabel, tab, qp, self)
class Example(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.tabWidget = QtWidgets.QTabWidget()
layout.addWidget(self.tabWidget)
self.tabBar = AnimatedTabBar(self.tabWidget)
self.tabWidget.setTabBar(self.tabBar)
self.tabWidget.addTab(QtWidgets.QCalendarWidget(), 'tab 1')
self.tabWidget.addTab(QtWidgets.QTableWidget(4, 8), 'tab 2')
self.tabWidget.addTab(QtWidgets.QGroupBox('Group'), 'tab 3')
self.tabWidget.addTab(QtWidgets.QGroupBox('Group'), 'tab 4')
self.setStyleSheet('''
QTabBar {
qproperty-hoverColor: rgb(128, 150, 140);
qproperty-normalColor: rgb(150, 198, 170);
qproperty-selectedColor: lightgreen;
}
''')
Some final notes:
I only implemented the top tab bar orientation, if you want to use tabs in the other directions, you'll have change the margins and rectangle adjustments;
remember that using stylesheets will break the appearence of the arrow buttons;(when tabs go beyond the width of the tab bar), you'll need to set them carefully
painting of movable (draggable) tabs is broken;
right now I don't really know how to fix that;

How to activate a particular tab programatically

I would like to change a tab when a button is clicked. How to switch tabs?
app.layout = html.Div(children=[
html.H1(children=title),
dcc.Markdown(header),
dcc.Tabs(id='graphs', children=[
dcc.Tab(label='Run', children=html.Div(children=form), value=10),
dcc.Tab(id='result', label='Result', children=graphs, value=1)],
value=10)])
#app.callback(Output('result', 'children'),
[Input('run_btn', 'n_clicks')],
inputs)
def call_simulation(clicks, *params):
params = dict(zip(parameter_mask.keys(), params))
if clicks is not None:
print(params)
try:
simulation(params)
SWITCH TO RESULT TAB
except Exception as e:
print(e)
return html.Div(children=["The simulation produced an error with this particular parameterisation", str(type(e)), str(e)])
return generate_graph_layout(newest_subdirectory('./result', ''))
else:
return html.Div()
You need a callback to set the value attribute of the dcc.Tabs element to the value attribute of the tab you want to switch to. So in your example you would need something like:
#app.callback(Output('graphs', 'value'),
[Input('run_btn', 'n_clicks')],
inputs)
def switch_tab(clicks, *params):
if clicks is not None:
return 1 # where 1 is the value of your results tab

IPython notebook widgets using interactive

I'm having trouble creating widgets in a Jupyter notebook that update when other widget values are changed. This is the code I've been playing around with:
from ipywidgets import interact, interactive, fixed
import ipywidgets as widgets
from IPython.display import display
def func(arg1,arg2):
print arg1
print arg2
choice = widgets.ToggleButtons(description='Choice:',options=['A','B'])
display(choice)
metric = widgets.Dropdown(options=['mercury','venus','earth'],description='Planets:')
text = widgets.Text(description='Text:')
a = interactive(func,
arg1=metric,
arg2=text,
__manual=True)
def update(*args):
if choice.value == 'A':
metric = widgets.Dropdown(options=['mercury','venus','earth'],description='Planets:')
text = widgets.Text(description='Text:')
a.children = (metric,text)
else:
metric = widgets.Dropdown(options=['monday','tuesday','wednesday'],description='Days:')
text2 = widgets.Textarea(description='Text2:')
a.children = (metric,text2)
choice.observe(update,'value')
display(a)
The resulting widgets metric and text do change based whether A or B is selected, but the problem is that the "Run func" button goes away as soon as I change to B. I've tried adding the __manual attribute immediately before display(a), adding it within update, and several other places. How do I change the children of the widget box without overwriting the fact that I want to manually run the function?

PyQt stacked widget not moving to next page until function ends

Hi i have a program that when a button is pressed it should move to the next stacked widget replace some text in some labels and then execute some functions but this is not working and moves to the next page when the functions completes
The code is :
QtCore.QObject.connect(self.StartBtn, QtCore.SIGNAL(_fromUtf8("clicked()")), self.start) #Start
def nextPage(self):
current_page = self.stackedWidget.currentIndex()
i = int(current_page) + 1
self.stackedWidget.setCurrentIndex(i)
def start(self):
self.nextPage()
self.animation()
self.runFunctions()
def runFunctions(self):
try:
self.DbLabel.setText(_translate("MainWindow", "Checking Database", None))
if checkDb == True:
self.DbLabel.setText(_translate("MainWindow", "Checking Database ", None))
self.checkDbFun()
self.DbLabel.setText(_translate("MainWindow", "Database checked", None))
else:
self.checkedDbImg.setPixmap(QtGui.QPixmap(_fromUtf8("Files\\x.png")))
self.DbLabel.setText(_translate("MainWindow", "Database not checked", None))
except Exception as e:
self.AlertMessage(e)
def animation(self):
self.LoadingGif = QtGui.QLabel(MainWindow)
movie = QtGui.QMovie("Files\\loading.png")
self.LoadingGif.setMovie(movie)
self.LoadingGif.setAlignment(QtCore.Qt.AlignCenter)
self.gridLayout_2.addWidget(self.LoadingGif, 4, 1, 1, 1)
movie.start()
So what i want is to press StartBtn then move to next stacked widget page load the animation image and then run the functions
You probably need to let Qt process events in order for the tab change to take effect. You could do that two ways:
insert a qApp.processEvents() between the animation() and runFunctions() (qApp is in PyQt5.QtWidgets)
call runFunctions() via a single-shot timer: QTimer.singleShot(0, runFunctions), which will schuedule runFunctions via the event loop, so any pending events will first be processed (because runFunctions() is the latest added), then runFunctions() will get called. If you actually have params for runFunctions(), use a lambda.
I favor the first approach because I find it more clearly indicates what is happening (events need to be processed), but I recommend also adding a comment on that line that "so stack tab can change".
BTW you should be use the new-style notation for signals-slot connections, much cleaner, of the form "signal.connect(slot)":
self.StartBtn.clicked.connect(self.start)
So for approach #1 your code would look like this:
from PyQt5.QtWidgets import qApp
...
self.StartBtn.clicked.connect(self.start)
...
def start(self):
self.nextPage()
self.animation()
qApp.processEvents()
self.runFunctions()
...