【问题标题】:Giving a color to single tab consumes too much processing power为单个选项卡提供颜色会消耗过多的处理能力
【发布时间】:2018-09-02 23:39:48
【问题描述】:

经过大量研究,我设法在 PyQt5 (Python 3.6) 中自定义了QTabWidget,以便我可以为任意选项卡分配不同的颜色:

是的,我知道可以使用 CSS 选择器来操作某些选项卡,例如:

  • QTabBar::tab:selected
  • QTabBar::tab:hover
  • QTabBar::tab:selected
  • QTabBar::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) 函数。我运行文件...但所有选项卡都是绿色的:-(

一定是我做错了什么?

编辑:
显然标签没有着色,因为我将stylesheetsQPalette 更改混合在一起。有人建议我注释掉对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!仍然存在一个问题(边界),但结果是我们得到的最接近解决方案的结果。

【问题讨论】:

  • polishunpolish 很可能会导致发布新的绘制事件。因此,从paintEvent 中调用其中任何一个几乎肯定会导致您发现的“忙于绘画”循环。你可能想看看the answer to this question——尽管是C++而不是python
  • 嗨@G.M. , 非常感谢您。我试过你发给我的链接,但没有成功。我已经记录了我的试用(刷新此页面可以查看详细信息,有一个新的小章节“建议的解决方案”)。我做错了什么?
  • 我认为现在的问题是您将样式表与QPalette 更改混合在一起。据我回忆,样式表将在这种情况下使用。通过快速检查尝试删除(注释掉)对setStyleSheet 的所有调用,看看会发生什么。
  • 嗨@G.M. ,当我注释掉 setStyleSheet(...) 函数时,它确实有效。但后来我失去了所有的风格。这很烦人(我需要它们有几个原因)。任何其他保持样式不变的解决方案?
  • 这必须在哪些平台上运行?这将很重要很多,因为某些解决方案根本不起作用,具体取决于平台的具体情况和/或当前使用的小部件样式(有关更多详细信息,请参阅Qt FAQ )。为了克服一些问题,您可能必须强制执行融合小部件样式。

标签: python python-3.x pyqt pyqt5 qtabwidget


【解决方案1】:

编辑:在积累了很多 QStyle 经验后,由于最近发布的另一个问题,我突然想起了这个问题,并意识到为什么"first proposed solution" 链接在问题中也没有工作和我的(出于相同的原因,但使用不同的实现)。向下滚动以获取替代解决方案。

第一个(接受的)答案

几周前我偶然发现了类似的问题,然后我对 QStyle 的工作原理进行了一些研究。 这个概念是您将让 Qt 绘制整个小部件,但使用 QStyleOption 辅助类(几乎每种小部件都有一个)。

这是一个简单的示例(我更新了代码),使用了您使用的部分样式表。 这里唯一的问题是标签边框没有正确使用样式表(我无法找到 QStyle 是如何绘制它们的),所以半径更小,笔宽更细。

我对其进行了测试,它可以在不消耗资源的情况下运行。希望对你有帮助。

class TabBar(QtWidgets.QTabBar):
    def __init__(self, parent):
        QtWidgets.QTabBar.__init__(self, parent)
        self.colorIndexes = parent.colorIndexes
        self.setStyleSheet('''
            QTabBar {
                font-family: Courier;
                font-size: 12pt;
            }
            QTabBar::tab {
                min-height: 40px;
                padding: 2px 8px;
            }
            ''')

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        qp.setRenderHints(qp.Antialiasing)
        option = QtWidgets.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, QtGui.QColor(QtCore.Qt.green)))
            palette.setColor(palette.Window, QtGui.QColor(QtCore.Qt.blue))
            option.palette = palette
            self.style().drawControl(QtWidgets.QStyle.CE_TabBarTab, option, qp)


class TabWidget(QtWidgets.QTabWidget):
    def __init__(self):
        QtWidgets.QTabWidget.__init__(self)
        self.setStyleSheet('''
            QTabWidget::pane {
                border: 2px solid blue;
                border-radius: 6px;
            }
            QTabWidget::tab-bar {
                left: 5px;
            }
            ''')
        self.colorIndexes = {
            1: QtGui.QColor(QtCore.Qt.red), 
            3: QtGui.QColor(QtCore.Qt.blue), 
            }
        self.setTabBar(TabBar(self))
        for i in range(5):
            w = QtWidgets.QWidget()
            self.addTab(w, 'tab {}'.format(i))


app = QtWidgets.QApplication(sys.argv)
QtWidgets.QApplication.setStyle('Fusion')
w = TabWidget()
w.show()
sys.exit(app.exec_())

注意:此示例仅适用于 Fusion 样式。 Breeze 不使用palette.Button,而是使用palette.Window;这意味着您可能能够找到其他样式的其他调色板角色组合,这可能会产生更符合您要求的结果。 我不知道是否真的可以通过 QStyle 绘制标签边框;如果您绝对需要边框,另一种方法是自己绘制边框,从QStyle.subElementRect() 获取各种内容大小。

替代(更新和改进)解决方案

问题在于,在使用 Qt 的样式表时,QStyle 函数的可选 widget 参数真的很重要,因为它们几乎完全依赖于小部件的样式表来绘制其形状和颜色(并计算其指标),而通常忽略调色板。

我想添加一个替代答案,一个实际上是一个小“hack”的解决方法,但最重要的是,通过完全按照预期绘制标签栏来解决与标签边框的不一致。
此外,它似乎与样式无关:我已经尝试过使用 Breeze、Oxygen、Windows 和 Fusion 样式,它总是给出相同的预期结果。

诀窍是创建一个充当“代理”的“私有”QTabBar 小部件(没有父级,以确保不会显示它),并对其应用自定义样式表,该小部件具有默认背景放;然后,如果要绘制的选项卡是“彩色”选项卡之一,它使用 that 内部 QTabBar 小部件作为drawControl 函数的参数。我创建了一个示例,可以用不同的颜色为每个选项卡着色,但如果您不需要那种复杂程度,显然可以只使用一个。
这里的重要区别是我们使用的是普通的 QPainter 而不是 QStylePainter,它的函数不允许我们将另一个小部件设置为参数。

def get_QTabBar_style(background='#00ff00'):
    styleStr = str('''
        QTabBar {{
            background: #00ffffff;
            color: #ff000000;
            font-family: Courier;
            font-size: 12pt;
        }}
        QTabBar::tab {{
            background: {};
            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;
        }}
    '''.format(background))

    return styleStr


class MyTabBar(QtWidgets.QTabBar):
    def __init__(self, parent):
        QtWidgets.QTabBar.__init__(self, parent)
        self.setStyleSheet(get_QTabBar_style())
        self.__coloredTabs = {}

    def colorTab(self, index, color='#ff0000'):
        if not 0 <= index < self.count():
            return
        proxy = self.__coloredTabs.get(index)
        if not proxy:
            proxy = self.__coloredTabs[index] = QtWidgets.QTabBar()
        proxy.setStyleSheet(get_QTabBar_style(color))
        self.update()

    def uncolorTab(self, index):
        try:
            self.__coloredTabs.pop(index)
            self.update()
        except:
            return

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        opt = QtWidgets.QStyleOptionTab()

        for i in range(self.count()):
            self.initStyleOption(opt, i)
            self.style().drawControl(
                QtWidgets.QStyle.CE_TabBarTabShape, opt, painter, 
                self.__coloredTabs.get(i, self))
            self.style().drawControl(
                QtWidgets.QStyle.CE_TabBarTabLabel, opt, painter, self)


class MyTabWidget(QtWidgets.QTabWidget):
    def __init__(self):
        QtWidgets.QTabWidget.__init__(self)
        self.setStyleSheet(get_QTabWidget_style())
        tabBar = MyTabBar(self)
        self.setTabBar(tabBar)
        self.colorTab = tabBar.colorTab
        self.uncolorTab = tabBar.uncolorTab

如您所见,结果几乎是完美的(除了标签栏和标签内容之间的小边距,恐怕这取决于样式和操作系统)。

【讨论】:

  • 我刚刚意识到绘图问题是由于我只绘制选项卡,而不是完整的小部件。这也意味着如果需要,您需要手动绘制角和标签按钮(包括关闭按钮)。回家后我会更新答案。
  • 您好 Musicamante,非常感谢您的回答。我已将您的代码插入到我自己的示例代码中,但显然我的样式表造成了麻烦。请看一下Second proposed solution - 我在问题中添加的新章节,完全专注于您的回答:-)
  • 好吧,我没明白你想“限制”使用的样式。似乎 Fusion QStyle 使用按钮作为“工厂”来绘制标签。您只需将第一个 setColor() 参数更改为palette.Button,它就可以完全按照您的需要工作。 :-) 顺便说一句,请记住 QPalette 通常不能很好地处理样式表。
  • 嗨@musicamente。非常感谢。我已经更新了示例代码(在Second proposed solution 章节中)。我已经运行了它,但仍然遇到问题。我需要停用我的样式表才能正确地为标签着色...
  • 你好,我更新了答案,结果和你的要求差不多,但还是有一些小问题(比如tab边框宽度)。
猜你喜欢
  • 2013-09-23
  • 2018-04-16
  • 1970-01-01
  • 2018-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-01
  • 1970-01-01
相关资源
最近更新 更多