为单个标签指定颜色会消耗太多处理能力

K.M*_*ier 4 python pyqt python-3.x qtabwidget pyqt5

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

在此处输入图片说明

是的,我知道可以使用 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_())

''''''
Run Code Online (Sandbox Code Playgroud)

代码解释

1. 动态样式表
我有一个用于 QTabWidget 的样式表和一个用于 QTabBar 的样式表。魔法在最后一个。选项卡的背景颜色(由 CSS-selector 表示QTabBar::tab)通常是 green #00ff00。但是当colorToggle属性打开时,颜色设置为 red #ff0000

 
2. class MyTabBar
我子类QTabBar成一个新类MyTabBar。这样,我可以做两件事:

  • 我添加了一个函数colorTab(index),以便外部代码可以调用它来为任意选项卡着色。

  • 我覆盖了该paintEvent(event)功能,以便我可以在所选选项卡上应用颜色。

colorTab(index)函数只需要一个索引并将其添加到列表中。就是这样。该列表将在被覆盖的paintEvent(event)函数中检查。

检查列表后,paintEvent(event)函数决定是否应该设置或清除属性"colorToggle"

    self.setProperty("colorToggle", True)
Run Code Online (Sandbox Code Playgroud)

设置(或清除)此属性后,该paintEvent(event)函数继续绘制实际选项卡:

    self.style().unpolish(self)
    self.style().polish(self)

    painter.drawControl(QStyle.CE_TabBarTabShape, opt)
    painter.drawControl(QStyle.CE_TabBarTabLabel, opt)
Run Code Online (Sandbox Code Playgroud)

 

我注意到了这一点,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)
Run Code Online (Sandbox Code Playgroud)

在被覆盖的paintEvent(event)函数内部。它们需要一些执行时间,这是一个问题,因为paintEvent 函数被非常定期地调用。对于这个简单示例,我的处理器运行速度为 14%(我有一个 4Ghz 水冷 i7 处理器)。这种处理器负载简直是不能接受的。


平台/环境

我正在运行:

  • 蟒蛇 3.6.3
  • PyQt5
  • Windows 10(但如果它适用于 Linux,请随时发布您的解决方案)

显然,小部件样式似乎很重要。在示例代码的最后几行,您可以看到:

    QApplication.setStyle(QStyleFactory.create('Fusion'))
Run Code Online (Sandbox Code Playgroud)

该小部件样式应该始终如一——无论是在 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)
Run Code Online (Sandbox Code Playgroud)

现在我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_())

''''''
Run Code Online (Sandbox Code Playgroud)

结果非常接近预期的结果:

在此处输入图片说明

Musicamante 说:

这里唯一的问题是选项卡边框不使用样式表(我无法找到 QStyle 如何绘制它们),因此半径更小,笔宽更细。

非常感谢@musicamante!还有一个问题(边界),但结果是我们得到的最接近解决方案的结果。

mus*_*nte 5

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

第一个(接受的)答案

几周前我偶然发现了类似的问题,然后我研究了一些 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_())
Run Code Online (Sandbox Code Playgroud)

注意:此示例仅适用于 Fusion 风格。Breeze 不使用palette.Button而是使用palette.Window;这意味着您可能能够在其他样式中找到其他调色板角色组合,这可能会产生更符合您要求的结果。我不知道是否真的可以通过 QStyle 绘制选项卡边框;如果您绝对需要边框,另一种选择是自己绘制它们,从QStyle.subElementRect().

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

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

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

诀窍是创建一个充当“代理”的“私有” QTabBar 小部件(没有父级,以确保它不会显示),并对其应用自定义样式表,该样式表具有默认背景设置;然后,如果要绘制的选项卡是“彩色”选项卡之一,则它使用内部 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
Run Code Online (Sandbox Code Playgroud)

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