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;
Related
#After clicking the button_next, nothing happens, the interface dosnt change at all
#After i clicked the button_next i would like the text to change which includes the input_name Entry()
def stage_0():
label_welcome = Label(text= "Welcome to Calorie counter", font =("Arial",24,"bold"),fg = "blue", bg ="#FFFF00")
label_welcome.pack(side = "top")
label_name=Label(text ="What is your name?",font = 18, bg ="#FFFF00")
label_name.pack()
input_name = Entry()
input_name.pack()
button_next = Button(text = "next",font = 18,command = name_input)
button_next.pack()
def name_input():
name = input_name.get()
print(name)
if name.isalpha()== True:
label_welcome["text"]= f"Nice to meet you {input_name.get()}!"
button_next.pack_forget()
input_name.pack_forget()
label_name["text"] ="What is your age"
scale_age = Scale(from_=0, to=100, orient = HORIZONTAL)
scale_age.pack()
else:
label_welcome["text"]= "Only alphabets allowed. Please try again"
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.
I've got a problem; I have boxes with text inside. The boxes have set sizes, but the text in them can chainge. I don't want the text going outside of them. Is there a build-in way or another way to get the text to stop at the edge of an box?
Before you blit text on the screen you can check it dimensions with get_rect() method. And depending on it you can choose whether to display it or not. I made a simple example:
import pygame as pg
pg.init()
clock = pg.time.Clock()
win = pg.display.set_mode((800, 600))
FONT = pg.font.SysFont("Times New Roman", 20, 1, 1)
class Field(pg.Rect):
def __init__(self, rect, text=None):
super().__init__(rect)
self.text = text
def get_text(self):
return self.text
def check_bounds(self, text_rect):
if text_rect.width > self.width:
return False
return True
def redraw(win):
win.fill(pg.Color("white"))
pg.draw.rect(win, pg.Color("green"), button_1, 1)
text = FONT.render("Some textdasdsdsafdss", 1, pg.Color("red"))
if button_1.check_bounds(text.get_rect()):
win.blit(text, (button_1.x, button_1.y))
else:
text = FONT.render("Text is too long!", 1, pg.Color("red"))
win.blit(text, (button_1.x, button_1.y))
pg.display.update()
def main_loop(win):
clock.tick(10)
for event in pg.event.get():
if event.type == pg.QUIT:
return False
redraw(win)
return True
button_1 = Field((100, 100, 170, 70))
while main_loop(win):
pass
pg.quit()
I hope this answers your question
EDIT: Answer for the question in comments by OP
Trim the user input to fit the textbox, add the following:
if len(user_input) >= MAX_STRING_LEN:
user_input = user_input[0:MAX_STRING_LEN]
text = FONT.render(user_input, 1, pg.Color("red"))
user_input is the string you loaded from the file and the MAX_STRING_LEN is a constant. If you have textboxes with different sizes you will need more constants.
In this case you do not need else part which blits "text is too long".
using urwid, I'm trying to separate the highlight/walk and cursor functionality of a Pile widget. How can I use up/down to change which widget is highlighted, while keeping the cursor in a different widget?
The default focus behavior couples the cursor with attribute (highlighting) behavior. The example below shows one way to decouple these, where a list of SelectableIcons retains the highlight feature, while the cursor is moved to a separate Edit widget. It does this via:
overriding the keypress method to update the focus where the cursor is not
wrapping each SelectableIcon in AttrMap that change their attribute based on their Pile's focus_position
after changing the SelectableIcon attributes, the focus (cursor) is set back to the Edit widget via focus_part='body'
self._w = ... is called to update all widgets on screen
There may be more concise ways of doing this, but this should be rather flexible.
import urwid
def main():
my_widget = MyWidget()
palette = [('unselected', 'default', 'default'),
('selected', 'standout', 'default', 'bold')]
urwid.MainLoop(my_widget, palette=palette).run()
class MyWidget(urwid.WidgetWrap):
def __init__(self):
n = 10
labels = ['selection {}'.format(j) for j in range(n)]
self.header = urwid.Pile([urwid.AttrMap(urwid.SelectableIcon(label), 'unselected', focus_map='selected') for label in labels])
self.edit_widgets = [urwid.Edit('', label + ' edit_text') for label in labels]
self.body = urwid.Filler(self.edit_widgets[0])
super().__init__(urwid.Frame(header=self.header, body=self.body, focus_part='body'))
self.update_focus(new_focus_position=0)
def update_focus(self, new_focus_position=None):
self.header.focus_item.set_attr_map({None: 'unselected'})
try:
self.header.focus_position = new_focus_position
self.body = urwid.Filler(self.edit_widgets[new_focus_position])
except IndexError:
pass
self.header.focus_item.set_attr_map({None: 'selected'})
self._w = urwid.Frame(header=self.header, body=self.body, focus_part='body')
def keypress(self, size, key):
if key == 'up':
self.update_focus(new_focus_position=self.header.focus_position - 1)
if key == 'down':
self.update_focus(new_focus_position=self.header.focus_position + 1)
if key in {'Q', 'q'}:
raise urwid.ExitMainLoop()
super().keypress(size, key)
main()
If you really need this, it probably makes sense to write your own widgets -- maybe based on some classes extending urwid.Text and urwid.Button
There is no real "highlight" feature in the widgets that come with urwid, there is only a "focus" feature, and it doesn't seem to be easy to decouple the focus highlight from the focus behavior.
You probably want to implement your own widgets with some sort of secondary highlighting.
In the following scala code I change foreground, horizontalAlignment and background to some values. However in the GUI these properties are not shown.
The horizontalAlignment remains centered.
The backgrould remains gray.
However the foreground (font color) changes according to the values.
How can I obtain the desired effects?
Thanks for any help!
import scala.swing._
object GuiTest extends SimpleSwingApplication {
def top = new MainFrame {
title = "Label Test"
val tempList = List("Celsius", "Fahrenheit", "Kelvin")
contents = bagPnl(tempList)
val fields = contents(0).peer.getComponents
val valuefields
= for (f <- 0 until fields.length / 2)
yield tempList(f) -> fields.apply(2 * f + 1).asInstanceOf[javax.swing.JLabel]
val tfm = valuefields.toMap[String, javax.swing.JLabel]
tfm.apply("Celsius").setText("35°C")
tfm.apply("Kelvin").setText("0 K")
}
def bagPnl(list: List[String]) = new GridBagPanel {
val gbc = new Constraints
gbc.insets = new Insets(3, 3, 3, 3)
var l = 0
for (title <- list) {
gbc.gridy = l
gbc.gridx = 0
/* title label */
add(new Label {
horizontalAlignment = Alignment.Left
foreground = java.awt.Color.RED
background = java.awt.Color.CYAN
text = title
}, gbc)
gbc.gridx = 1
/* value label */
val t = new Label {
horizontalAlignment = Alignment.Right
foreground = java.awt.Color.BLUE
background = java.awt.Color.YELLOW
name = title
}
t.background = java.awt.Color.GREEN
add(t, gbc)
l = l + 1
}
}
}
sorry I am not allowed to post images yet :-(
GridBagLayout is one hell of a layout manager. You'll be probably better of with GroupLayout, but there is no related panel type in Scala-Swing yet. (Here for an example).
The problem with the label positioning is that its alignment only makes sense when it is given more space than its preferred size. By default, the grid bag layout doesn't give it more space, and the centering is a result of its own alignment (not that of the label). The easiest here is to specify that the components can use up the horizontal space if available:
gbc.fill = GridBagPanel.Fill.Horizontal
The second questions concerns the background color of the label. Here is a related question. In short, by default the label is transparent and its background color ignored. You can switch to opaque painting:
new Label {
...
opaque = true
}