After lots of research, I managed to customize the QTabWidget
in PyQt5 (Python 3.6) such that I can assign a different color to an arbitrary tab:
Yes, I know that one can manipulate certain tabs using CSS-selectors like:
QTabBar::tab:selected
QTabBar::tab:hover
QTabBar::tab:selected
QTabBar::tab:!selected
But none of these selectors solves the actual problem I have. If I want to highlight the second tab - no matter if it is selected, hovered, ... - neither of these CSS-selectors help me.
I will now explain how I got it eventually working. After that, I'll show where the computation-intensive part is, and why I can't get that out. Hopefully you can help me to improve the efficiency.
The code
Below you can find the source code of my solution. To try it out yourself, just copy-paste the code into a new file (like tab_test.py
) and run it. Below the code you find more explanations.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
#########################################################
# STYLESHEET FOR QTABWIDGET #
#########################################################
def get_QTabWidget_style():
styleStr = str("""
QTabWidget::pane {
border-width: 2px;
border-style: solid;
border-color: #0000ff;
border-radius: 6px;
}
QTabWidget::tab-bar {
left: 5px;
}
""")
return styleStr
#########################################################
# STYLESHEET FOR QTABBAR #
#########################################################
def get_QTabBar_style():
styleStr = str("""
QTabBar {
background: #00ffffff;
color: #ff000000;
font-family: Courier;
font-size: 12pt;
}
QTabBar::tab {
background: #00ff00;
color: #000000;
border-width: 2px;
border-style: solid;
border-color: #0000ff;
border-bottom-color: #00ffffff;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
min-height: 40px;
padding: 2px;
}
QTabBar::tab:selected {
border-color: #0000ff;
border-bottom-color: #00ffffff;
}
QTabBar::tab:!selected {
margin-top: 2px;
}
QTabBar[colorToggle=true]::tab {
background: #ff0000;
}
""")
return styleStr
#########################################################
# SUBCLASS QTABBAR #
#########################################################
class MyTabBar(QTabBar):
def __init__(self, *args, **kwargs):
super(MyTabBar, self).__init__(*args, **kwargs)
self.__coloredTabs = []
self.setProperty("colorToggle", False)
def colorTab(self, index):
if (index >= self.count()) or (index < 0) or (index in self.__coloredTabs):
return
self.__coloredTabs.append(index)
self.update()
def uncolorTab(self, index):
if index in self.__coloredTabs:
self.__coloredTabs.remove(index)
self.update()
def paintEvent(self, event):
painter = QStylePainter(self)
opt = QStyleOptionTab()
painter.save()
for i in range(self.count()):
self.initStyleOption(opt, i)
if i in self.__coloredTabs:
self.setProperty("colorToggle", True)
self.style().unpolish(self)
self.style().polish(self)
painter.drawControl(QStyle.CE_TabBarTabShape, opt)
painter.drawControl(QStyle.CE_TabBarTabLabel, opt)
else:
self.setProperty("colorToggle", False)
self.style().unpolish(self)
self.style().polish(self)
painter.drawControl(QStyle.CE_TabBarTabShape, opt)
painter.drawControl(QStyle.CE_TabBarTabLabel, opt)
painter.restore()
#########################################################
# SUBCLASS QTABWIDGET #
#########################################################
class MyTabWidget(QTabWidget):
def __init__(self, *args, **kwargs):
super(MyTabWidget, self).__init__(*args, **kwargs)
self.myTabBar = MyTabBar()
self.setTabBar(self.myTabBar)
self.setTabsClosable(True)
self.setStyleSheet(get_QTabWidget_style())
self.tabBar().setStyleSheet(get_QTabBar_style())
def colorTab(self, index):
self.myTabBar.colorTab(index)
def uncolorTab(self, index):
self.myTabBar.uncolorTab(index)
'''=========================================================='''
'''| CUSTOM MAIN WINDOW |'''
'''=========================================================='''
class CustomMainWindow(QMainWindow):
def __init__(self):
super(CustomMainWindow, self).__init__()
# -------------------------------- #
# Window setup #
# -------------------------------- #
# 1. Define the geometry of the main window
# ------------------------------------------
self.setGeometry(100, 100, 800, 800)
self.setWindowTitle("Custom TabBar test")
# 2. Create frame and layout
# ---------------------------
self.__frm = QFrame(self)
self.__frm.setStyleSheet("QWidget { background-color: #efefef }")
self.__lyt = QVBoxLayout()
self.__frm.setLayout(self.__lyt)
self.setCentralWidget(self.__frm)
# 3. Insert the TabMaster
# ------------------------
self.__tabMaster = MyTabWidget()
self.__lyt.addWidget(self.__tabMaster)
# 4. Add some dummy tabs
# -----------------------
self.__tabMaster.addTab(QFrame(), "first")
self.__tabMaster.addTab(QFrame(), "second")
self.__tabMaster.addTab(QFrame(), "third")
self.__tabMaster.addTab(QFrame(), "fourth")
# 5. Color a specific tab
# ------------------------
self.__tabMaster.colorTab(1)
# 6. Show window
# ---------------
self.show()
''''''
'''=== end Class ==='''
if __name__ == '__main__':
app = QApplication(sys.argv)
QApplication.setStyle(QStyleFactory.create('Fusion'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
''''''
The code explained
1. Dynamic stylesheets
I've got a stylesheet for the QTabWidget and one for the QTabBar. The magic is in the last one. The background color of the tab (denoted by the CSS-selector QTabBar::tab
) is generally green #00ff00
. But when the colorToggle
property is on, the color is set to red #ff0000
.
2. class MyTabBar
I subclass QTabBar
into a new class MyTabBar
. In this way, I can do two things:
I add a function
colorTab(index)
such that external code can call it to color an arbitrary tab.I override the
paintEvent(event)
function such that I can apply the color on the chosen tabs.
The colorTab(index)
function simply takes an index and adds it to a list. That's it. The list will be checked in the overridden paintEvent(event)
function.
After checking the list, the paintEvent(event)
function decides whether it should set or clear the property "colorToggle"
:
self.setProperty("colorToggle", True)
After setting (or clearing) this property, the paintEvent(event)
function proceeds to paint the actual tab:
self.style().unpolish(self)
self.style().polish(self)
painter.drawControl(QStyle.CE_TabBarTabShape, opt)
painter.drawControl(QStyle.CE_TabBarTabLabel, opt)
I have noticed that
self.style().unpolish(self)
andself.style().polish(self)
consume a lot of processing power. But deleting them results in failure. I don't know any (less computational-intensive) alternative.
3. class MyTabWidget
I've also subclassed the QTabWidget
class. In its constructor, I replace the default QTabBar
by my own subclassed MyTabBar
. After that, I apply my stylesheets.
4. class CustomMainWindow
I create a main window (subclassed from QMainWindow
) to simply test the new Tab Widget. That's very simple. I instantiate MyTabWidget()
and insert some dummy tabs into it.
Then I color the second one (note: tab counting starts from 0).
The problem explained
The problem is all in the lines:
self.style().unpolish(self)
self.style().polish(self)
inside the overridden paintEvent(event)
function. They take some execution time, which is a problem because the paintEvent function gets called very regularly. My processor runs at 14% for this simple example (I have a 4Ghz watercooled i7 processor). Such processor load is simply unacceptable.
The platform/environment
I'm running on:
- Python 3.6.3
- PyQt5
- Windows 10 (but please feel free to post your solution if it works on Linux)
Apparently the widget-style seems to be important. On the last lines of the sample code, you can see:
QApplication.setStyle(QStyleFactory.create('Fusion'))
That widget-style should be consistently the same - both on Windows and Linux. But again - please feel free to post your solution if it works on another non-Fusion style.
First proposed solution
I was recommended to take a look here: Qt TabWidget Each tab Title Background Color
A solution is proposed: Subclass QTabBar
and override the paintEvent(event)
function. That's quite similar to the solution I already have above, but the code inside the paintEvent(event)
function is different. So I give it a try.
First, I translate the given C++ code into Python:
def paintEvent(self, event):
painter = QStylePainter(self)
opt = QStyleOptionTab()
for i in range(self.count()):
self.initStyleOption(opt, i)
if i in self.__coloredTabs:
opt.palette.setColor(QPalette.Button, QColor("#ff0000"))
painter.drawControl(QStyle.CE_TabBarTabShape, opt)
painter.drawControl(QStyle.CE_TabBarTabLabel, opt)
Now I replace my previous paintEvent(event)
function with this code. I run the file ... but all tabs are green :-(
There must be something I'm doing wrong?
EDIT :
Apparently the tab didn't color because I was mixing stylesheets
with QPalette
changes. I was suggested to comment out all calls to setStyleSheet(..)
and try again. Indeed, the intended tab gets the new color. But I lose all my styles... So this won't really help me.
Second proposed solution
Musicamante has proposed a solution based on QStyleOption
helper classes. Please look below to see his answer. I've inserted his solution into my own sample code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
#########################################################
# STYLESHEET FOR QTABWIDGET #
#########################################################
def get_QTabWidget_style():
styleStr = str("""
QTabWidget::pane {
border-width: 2px;
border-style: solid;
border-color: #0000ff;
border-radius: 6px;
}
QTabWidget::tab-bar {
left: 5px;
}
""")
return styleStr
#########################################################
# STYLESHEET FOR QTABBAR #
#########################################################
def get_QTabBar_style():
styleStr = str("""
QTabBar {
background: #00ffffff;
color: #ff000000;
font-family: Courier;
font-size: 12pt;
}
QTabBar::tab {
background: #00ff00;
color: #000000;
border-width: 2px;
border-style: solid;
border-color: #0000ff;
border-bottom-color: #00ffffff;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
min-height: 40px;
padding: 2px 12px;
}
QTabBar::tab:selected {
border-color: #0000ff;
border-bottom-color: #00ffffff;
}
QTabBar::tab:!selected {
margin-top: 2px;
}
QTabBar[colorToggle=true]::tab {
background: #ff0000;
}
""")
return styleStr
#########################################################
# SUBCLASS QTABBAR #
#########################################################
class MyTabBar(QTabBar):
def __init__(self, parent):
QTabBar.__init__(self, parent)
self.colorIndexes = parent.colorIndexes
def paintEvent(self, event):
qp = QPainter(self)
qp.setRenderHints(qp.Antialiasing)
option = QStyleOptionTab()
option.features |= option.HasFrame
palette = option.palette
for index in range(self.count()):
self.initStyleOption(option, index)
palette.setColor(palette.Button, self.colorIndexes.get(index, QColor(Qt.green)))
palette.setColor(palette.Window, QColor(Qt.blue))
option.palette = palette
self.style().drawControl(QStyle.CE_TabBarTab, option, qp)
#########################################################
# SUBCLASS QTABWIDGET #
#########################################################
class MyTabWidget(QTabWidget):
def __init__(self):
QTabWidget.__init__(self)
self.colorIndexes = {
1: QColor(Qt.red),
3: QColor(Qt.blue),
}
self.setTabBar(MyTabBar(self))
self.tabBar().setStyleSheet(get_QTabBar_style())
self.setStyleSheet(get_QTabWidget_style())
self.setTabsClosable(True)
'''=========================================================='''
'''| CUSTOM MAIN WINDOW |'''
'''=========================================================='''
class CustomMainWindow(QMainWindow):
def __init__(self):
super(CustomMainWindow, self).__init__()
# -------------------------------- #
# Window setup #
# -------------------------------- #
# 1. Define the geometry of the main window
# ------------------------------------------
self.setGeometry(100, 100, 800, 800)
self.setWindowTitle("Custom TabBar test")
# 2. Create frame and layout
# ---------------------------
self.__frm = QFrame(self)
self.__frm.setStyleSheet("QWidget { background-color: #efefef }")
self.__lyt = QVBoxLayout()
self.__frm.setLayout(self.__lyt)
self.setCentralWidget(self.__frm)
# 3. Insert the TabMaster
# ------------------------
self.__tabMaster = MyTabWidget()
self.__lyt.addWidget(self.__tabMaster)
# 4. Add some dummy tabs
# -----------------------
self.__tabMaster.addTab(QFrame(), "first")
self.__tabMaster.addTab(QFrame(), "second")
self.__tabMaster.addTab(QFrame(), "third")
self.__tabMaster.addTab(QFrame(), "fourth")
# 5. Show window
# ---------------
self.show()
''''''
'''=== end Class ==='''
if __name__ == '__main__':
app = QApplication(sys.argv)
QApplication.setStyle(QStyleFactory.create('Fusion'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
''''''
The result gets pretty close to the desired outcome:
Musicamante says:
The only issue here is that the tab border does not use stylesheets (I wasn't able to find how QStyle draws them), so the radius is smaller and the pen width is thinner.
Thank you very much @musicamante! There is still one issue (the borders) but the result is the closest we ever got to the solution.