Search code examples
tabspyqt5qtabwidgetqtabbar

QTabWidget Access Actual Tab (not the content widget)


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

Solution

  • 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;