【发布时间】:2018-09-02 23:39:48
【问题描述】:
经过大量研究,我设法在 PyQt5 (Python 3.6) 中自定义了QTabWidget,以便我可以为任意选项卡分配不同的颜色:
是的,我知道可以使用 CSS 选择器来操作某些选项卡,例如:
QTabBar::tab:selectedQTabBar::tab:hoverQTabBar::tab:selectedQTabBar::tab:!selected
但是这些选择器都不能解决我遇到的实际问题。如果我想突出显示第二个选项卡 - 无论它是否被选中、悬停...... - 这些 CSS 选择器都没有帮助我。
我现在将解释我是如何让它最终发挥作用的。在那之后,我将展示计算密集型部分在哪里,以及为什么我不能把它弄出来。希望您能帮助我提高效率。
代码
您可以在下面找到我的解决方案的源代码。要自己尝试,只需将代码复制粘贴到一个新文件(如tab_test.py)并运行它。您可以在代码下方找到更多解释。
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_())
''''''
代码解释
1.动态样式表
我有一个用于 QTabWidget 的样式表和一个用于 QTabBar 的样式表。魔术在最后一个。选项卡的背景颜色(由 CSS 选择器 QTabBar::tab 表示)通常为绿色 #00ff00。但是当colorToggle属性开启时,颜色设置为红色#ff0000。
2. MyTabBar 类
我将QTabBar 子类化为一个新类MyTabBar。这样,我可以做两件事:
我添加了一个函数
colorTab(index),以便外部代码可以调用它来为任意选项卡着色。我重写了
paintEvent(event)函数,这样我就可以在选定的选项卡上应用颜色。
colorTab(index) 函数只需要一个索引并将其添加到列表中。而已。将在覆盖的paintEvent(event) 函数中检查该列表。
检查列表后,paintEvent(event) 函数决定是否设置或清除属性"colorToggle":
self.setProperty("colorToggle", True)
设置(或清除)此属性后,paintEvent(event) 函数继续绘制实际的选项卡:
self.style().unpolish(self)
self.style().polish(self)
painter.drawControl(QStyle.CE_TabBarTabShape, opt)
painter.drawControl(QStyle.CE_TabBarTabLabel, opt)
我注意到
self.style().unpolish(self)和self.style().polish(self)消耗了大量的处理能力。但是删除它们会导致失败。我不知道任何(计算量较小的)替代方案。
3. MyTabWidget 类
我还继承了QTabWidget 类。在其构造函数中,我将默认的QTabBar 替换为我自己的子类MyTabBar。之后,我应用我的样式表。
4.类 CustomMainWindow
我创建了一个主窗口(从 QMainWindow 子类化)来简单地测试新的 Tab Widget。这很简单。我实例化 MyTabWidget() 并在其中插入一些虚拟标签。
然后我为第二个着色(注意:标签计数从 0 开始)。
问题解释
问题出在:
self.style().unpolish(self)
self.style().polish(self)
在被覆盖的paintEvent(event) 函数中。它们需要一些执行时间,这是一个问题,因为paintEvent 函数被非常频繁地调用。对于这个简单的示例,我的处理器以 14% 的速度运行(我有一个 4Ghz 水冷 i7 处理器)。这样的处理器负载简直是不可接受的。
平台/环境
我正在跑步:
- Python 3.6.3
- PyQt5
- Windows 10(但如果它适用于 Linux,请随时发布您的解决方案)
显然小部件样式似乎很重要。在示例代码的最后几行,您可以看到:
QApplication.setStyle(QStyleFactory.create('Fusion'))
小部件样式应该始终如一 - 在 Windows 和 Linux 上都是一样的。但同样 - 如果它适用于另一种非 Fusion 风格,请随时发布您的解决方案。
首次提出的解决方案
我被推荐看这里:Qt TabWidget Each tab Title Background Color
提出了一个解决方案:子类QTabBar并覆盖paintEvent(event)函数。这与我上面已有的解决方案非常相似,但paintEvent(event) 函数中的代码不同。所以我试试看。
首先,我将给定的 C++ 代码翻译成 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)
现在我用这段代码替换我之前的paintEvent(event) 函数。我运行文件...但所有选项卡都是绿色的:-(
一定是我做错了什么?
编辑:
显然标签没有着色,因为我将stylesheets 与QPalette 更改混合在一起。有人建议我注释掉对setStyleSheet(..) 的所有调用,然后再试一次。实际上,预期的选项卡会获得新颜色。但是我失去了所有的风格......所以这对我没有帮助。
第二个提议的解决方案
Musicamante 提出了一个基于QStyleOption 助手类的解决方案。请往下看,看看他的回答。我已将他的解决方案插入到我自己的示例代码中:
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_())
''''''
结果非常接近预期的结果:
Musicamante 说:
这里唯一的问题是标签边框不使用样式表(我无法找到 QStyle 是如何绘制它们的),所以半径更小,笔宽更细。
非常感谢@musicamante!仍然存在一个问题(边界),但结果是我们得到的最接近解决方案的结果。
【问题讨论】:
-
polish和unpolish很可能会导致发布新的绘制事件。因此,从paintEvent中调用其中任何一个几乎肯定会导致您发现的“忙于绘画”循环。你可能想看看the answer to this question——尽管是C++而不是python。 -
嗨@G.M. , 非常感谢您。我试过你发给我的链接,但没有成功。我已经记录了我的试用(刷新此页面可以查看详细信息,有一个新的小章节“建议的解决方案”)。我做错了什么?
-
我认为现在的问题是您将样式表与
QPalette更改混合在一起。据我回忆,样式表将在这种情况下使用。通过快速检查尝试删除(注释掉)对setStyleSheet的所有调用,看看会发生什么。 -
嗨@G.M. ,当我注释掉
setStyleSheet(...)函数时,它确实有效。但后来我失去了所有的风格。这很烦人(我需要它们有几个原因)。任何其他保持样式不变的解决方案? -
这必须在哪些平台上运行?这将很重要很多,因为某些解决方案根本不起作用,具体取决于平台的具体情况和/或当前使用的小部件样式(有关更多详细信息,请参阅Qt FAQ )。为了克服一些问题,您可能必须强制执行融合小部件样式。
标签: python python-3.x pyqt pyqt5 qtabwidget