0

In this image:

enter image description here

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
SamG101
  • 488
  • 1
  • 7
  • 18
  • 1
    Tabs are not widgets, nor actual objects. Can you clarify exactly what you want to do with it? – musicamante Mar 01 '20 at 15:57
  • Could you explain to me better what you want to do, ask about its underlying objective and not about how to implement a possible solution that nobody guarantees to work. I think you have a [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – eyllanesc Mar 01 '20 at 16:10
  • @musicamante when I hover on one tab, I would like the colour to change with an animation, so I can't use style-sheets. I get the index that the tab is at on the hover event (sub-classed and created); I just cant get the actual object of the tab so I can animate the change of the colour of it. – SamG101 Mar 02 '20 at 08:57
  • I have come across a `QTabBar.tabList(...)` whilst analysing the `QTabBar.cpp`, but maybe this is private so not accessible? (I have next to no knowledge of programming in C++, I use Python). If it's not private, then how can I access it from the `QTabBar`? – SamG101 Mar 02 '20 at 10:11
  • @musicamante what are tabs then? – SamG101 Mar 02 '20 at 10:21
  • @SamG101 As I already told you, the tabs of a QTabBar are not widgets, they are not even objects. They are an abstract representation of the tab bar list, but it's up to the QTabBar itself to actually paint them according to the style. No, you can't access the `tabList` from Python, as it's private, but all of its properties are accessible through QTabBar functions. The only solution you have is to override the paintEvent, I'm preparing a small example. – musicamante Mar 02 '20 at 10:46

1 Answers1

1

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.

A cool animated tab bar

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;
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks, this is exactly what I needed – SamG101 Mar 03 '20 at 14:52
  • One question: how is the border radius painted on? Which lines of code do this? Thanks – SamG101 Mar 03 '20 at 15:05
  • @SamG101 I set that value in the `pen = ...` lines, which is then applied when using `qp.setPen()`. For simplicity I used a border only for the selected tab, while the others won't have any (QtCore.Qt.NoPen), but you could set that property too following the same concepts as the `selectedColor`, possibly using something like `selectedBorder` and `normalBorder`. – musicamante Mar 03 '20 at 15:09
  • Ah sorry, I meant the curvature of the border of the tabs that is created as opposed to the colour. – SamG101 Mar 03 '20 at 16:16
  • @SamG101 That's `drawRoundedRect()`. If you want to know more about painting in Qt, you can read the documentation about [QPainter](https://doc.qt.io/qt-5/qpainter.html). – musicamante Mar 03 '20 at 16:38