当 Qt.UIEffect.UI_AnimateCombo 设置为 False 时,QComboBox 在更改样式表时出现意外行为

发布于 2025-01-12 00:42:32 字数 2084 浏览 0 评论 0原文

我的目标是制作一个组合框,仅当鼠标悬停在其上方时才显示向下箭头。为此,我对 QComboBox 进行了子类化并重新实现了 EnterEvent 和 LeaveEvent,这将相应地更改其样式表。它按预期工作,当窗口显示时,向下箭头隐藏,当鼠标悬停在组合框上方时,向下箭头显示。然后,当鼠标单击组合框文本时,下拉列表会显示出来,向下箭头再次变得不可见(我可以处理,但如果有人有解决方案,我会洗耳恭听)。主要问题是,当鼠标单击下拉箭头时,它不仅会像鼠标单击文本时那样隐藏下拉箭头,还会隐藏文本和下拉箭头。我做了一些测试来找出导致问题的原因,似乎在 QApplication 上调用 setEffectEnabled(Qt.UIEffect.UI_AnimateCombo, False) 是问题所在,尽管我不知道禁用下拉动画与单击时文本消失有何关系在下拉箭头上。另外,如果有人有更优雅的方式来做我想做的事,我会洗耳恭听:)。这是示例代码:

import sys

from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QEnterEvent
from PyQt5.QtWidgets import QApplication, QMainWindow, QComboBox, QWidget


class TestComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.default_stylesheet = ''' 
        QComboBox {
            background-color: rgba(0, 0, 0, 0);
        }
        QComboBox QAbstractItemView {
            background-color: white;
            min-width: 150px;
        }'''

        self.hide_down_arrow_stylesheet = '''QComboBox::down-arrow { \
                                             background-color: rgba(0, 0, 0, 0);}'''

        self.setStyleSheet(self.default_stylesheet + self.hide_down_arrow_stylesheet)

    def enterEvent(self, event: QEnterEvent) -> None:
        self.setStyleSheet(self.default_stylesheet)
        super().enterEvent(event)

    def leaveEvent(self, event: QEvent) -> None:
        self.setStyleSheet(self.default_stylesheet + self.hide_down_arrow_stylesheet)
        super().leaveEvent(event)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.widget = QWidget(self)
        self.setCentralWidget(self.widget)

        self.combobox = TestComboBox(self.widget)
        self.combobox.resize(180, 30)
        self.combobox.insertItem(0, "item 1")
        self.combobox.insertItem(0, "item 2")

        self.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setEffectEnabled(Qt.UIEffect.UI_AnimateCombo, False)
    window = MainWindow()
    sys.exit(app.exec())

My goal was to make a combo box which shows the down arrow only when the mouse is hovering above it. To do that I subclassed QComboBox and reimplemented enterEvent and leaveEvent which would change its stylesheet accordingly. It works just as expected, when to window shows up, down arrow is hidden and when the mouse hovers above combo box, the down arrow shows up. Then, when the mouse clicks on combo box text, the drop down list shows up and the down arrow becomes invisible again (which I can deal with, but if someone has a solution I'm all ears). The main problem is when the mouse clicks on the drop down arrow, instead of just hiding the drop down arrow, as it did when mouse clicked on the text, it also hides the text along with the drop down arrow. I did some testing to find out what's causing the problem and it seems that calling setEffectEnabled(Qt.UIEffect.UI_AnimateCombo, False) on QApplication is the issue, although I have no idea how disabling drop down animation is related to making text disappear when clicking on drop down arrow. Also, if someone has more elegant way of doing what I want, again, I'm all ears :). Here's the example code:

import sys

from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QEnterEvent
from PyQt5.QtWidgets import QApplication, QMainWindow, QComboBox, QWidget


class TestComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.default_stylesheet = ''' 
        QComboBox {
            background-color: rgba(0, 0, 0, 0);
        }
        QComboBox QAbstractItemView {
            background-color: white;
            min-width: 150px;
        }'''

        self.hide_down_arrow_stylesheet = '''QComboBox::down-arrow { \
                                             background-color: rgba(0, 0, 0, 0);}'''

        self.setStyleSheet(self.default_stylesheet + self.hide_down_arrow_stylesheet)

    def enterEvent(self, event: QEnterEvent) -> None:
        self.setStyleSheet(self.default_stylesheet)
        super().enterEvent(event)

    def leaveEvent(self, event: QEvent) -> None:
        self.setStyleSheet(self.default_stylesheet + self.hide_down_arrow_stylesheet)
        super().leaveEvent(event)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.widget = QWidget(self)
        self.setCentralWidget(self.widget)

        self.combobox = TestComboBox(self.widget)
        self.combobox.resize(180, 30)
        self.combobox.insertItem(0, "item 1")
        self.combobox.insertItem(0, "item 2")

        self.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setEffectEnabled(Qt.UIEffect.UI_AnimateCombo, False)
    window = MainWindow()
    sys.exit(app.exec())

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

一花一树开 2025-01-19 00:42:32

可能存在一些内部且复杂的错误,与您只设置了单个子控件属性这一事实相关,但是文档专门解决了这个方面:

注意:对于复杂的小部件,例如 QComboBox 和 QScrollBar,如果自定义了一个属性或子控件,则所有其他属性或子控件都必须自定义为好吧。

请注意,在大多数情况下,这也与没有 布局管理器 有关已设置,并且在我的大多数测试中,只要使用正确的布局,问题就会自动消失(正如您总是应该的那样,即使您只使用一个子元素小部件)。

class MainWindow(QMainWindow):
    def __init__(self):
        # ...
        layout = QVBoxLayout(self.widget)
        layout.addWidget(self.combobox)

现在,即使使用布局,我仍然能够在某些条件下重现该问题。这是由于一开始所解释的:如果你设置了一个复杂的小部件的属性,你必须设置所有的属性;如果不这样做可能会导致意外的行为。

在这种特定情况下,我相信 QStyleSheetStyle (它是在受 QSS 影响的任何小部件上自动设置的私有样式,包括继承的小部件)无法恢复画家的笔,并且它使用组合标签的箭头颜色,但它仅在某些(可能是“随机”)条件下才会这样做。

重点仍然是:有些属性未设置,特别是 drop-down 伪元素。
这样做的结果是,为该伪元素设置边框会导致将箭头图像重置为空白图像,但我们可以将其用作实际优势;因为我们知道设置边框会导致无箭头图像,所以我们可以相应地更新样式表:

class TestComboBox(QComboBox):
    default_stylesheet = ''' 
        TestComboBox {
            color: black;
            selection-color: black;
        }
        
        TestComboBox QAbstractItemView {
            background-color: white;
            min-width: 150px;
        }
        '''

    non_hover_stylesheet = '''
        TestComboBox::drop-down {
            border: none;
        }
        '''

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setHoverState(False)

    def setHoverState(self, state):
        qss = self.default_stylesheet
        if not state:
            qss += self.non_hover_stylesheet
        self.setStyleSheet(qss)

    def enterEvent(self, event):
        super().enterEvent(event)
        self.setHoverState(True)

    def leaveEvent(self, event):
        super().leaveEvent(event)
        self.setHoverState(False)

进一步说明:

  • 我删除了 selection-background-colorbackground-color 属性,因为它们会导致与某些样式(特别是“Fusion”样式,它被认为是 QSS 的参考样式,并且应该始终进行检查)不一致的行为,因为这些属性经常被忽略使用阿尔法水平;我无法使用“WindowsVista”样式对其进行测试,但要点仍然是:如果您想要透明的背景颜色,它必须与实际背景一致;考虑使用 QApplication 的 palette()获取 HighlightHighlightedText 角色的颜色,并最终使用这些颜色(不带 alpha 级别)来设置实际背景;
  • 我将 QSS“模板”设置为类属性:因为我们可以假设样式表被设置为类默认值,所以将其定义为实例属性没有什么意义;
  • 这显然(也不可以)考虑继承样式表;虽然我专门使用了 TestComboBox 类选择器而不是 QComboBox,但请记住,如果您设置样式表(扩展其他 QComboBox 属性或伪元素) /states) 对于任何父级或应用程序,您可能会得到不一致和意外的行为;
  • (不相关,但仍然很重要)在小部件的 __init__

There's probably some internal and complex bug related to the fact that you only have set a single sub-control property, but the documentation specifically addresses this aspect:

Note: With complex widgets such as QComboBox and QScrollBar, if one property or sub-control is customized, all the other properties or sub-controls must be customized as well.

Be aware that in most cases, this is also related to the fact that no layout manager was set, and in most of my tests the problem automatically disappears as long as a proper layout is used (as you always should, even if you're using only one child widget).

class MainWindow(QMainWindow):
    def __init__(self):
        # ...
        layout = QVBoxLayout(self.widget)
        layout.addWidget(self.combobox)

Now, I was able to still reproduce the problem under certain conditions, even with a layout. This is due to what was explained in the beginning: if you set a property of a complex widget, you have to set all of them; failing to do so might result in unexpected behavior.

In this specific case, I believe that the QStyleSheetStyle (which is a private style automatically set on any widget that is affected by a QSS, including inherited ones) fails to restore the painter's pen, and it uses the arrow's color for the combo label, but it does so only under certain (possibly, "random") conditions.

The point remains: there are properties that are not set, specifically the drop-down pseudo element.
A consequence of this is that setting the border for that pseudo element results in resetting the arrow image to a blank one, but we can use that as an actual advantage; since we know that setting a border results in a no arrow image, we can update the style sheet accordingly:

class TestComboBox(QComboBox):
    default_stylesheet = ''' 
        TestComboBox {
            color: black;
            selection-color: black;
        }
        
        TestComboBox QAbstractItemView {
            background-color: white;
            min-width: 150px;
        }
        '''

    non_hover_stylesheet = '''
        TestComboBox::drop-down {
            border: none;
        }
        '''

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setHoverState(False)

    def setHoverState(self, state):
        qss = self.default_stylesheet
        if not state:
            qss += self.non_hover_stylesheet
        self.setStyleSheet(qss)

    def enterEvent(self, event):
        super().enterEvent(event)
        self.setHoverState(True)

    def leaveEvent(self, event):
        super().leaveEvent(event)
        self.setHoverState(False)

Further notes:

  • I removed the selection-background-color and background-color properties, as they result in an inconsistent behavior with certain styles (specifically, the "Fusion" style, which is considered the reference one for QSS and should always be checked) because those properties are often used ignoring the alpha level; I cannot test it with the "WindowsVista" style, but the point remains: if you want a transparent background color, it must be consistent with the actual background; consider using the QApplication's palette() to get the colors of Highlight and HighlightedText roles, and eventually use those colors (without the alpha level) to set the actual backgrounds;
  • I made the QSS "templates" as class attributes: since we can assume that the stylesheet is set as a class default, defining it as an instance attribute makes little sense;
  • this obviously does not (nor can) consider inherited stylesheets; while I specifically used the TestComboBox class selector instead of QComboBox, but remember that if you set a stylesheet (that expands other QComboBox properties or pseudo elements/states) to any parent or the application, you may get inconsistent and unexpected behavior;
  • (unrelated, but still important) it's good practice to not call self.show() (or self.setVisible(True)) within the __init__ of a widget;
深海不蓝 2025-01-19 00:42:32

我通过阅读文档 https:// /doc.qt.io/qt-5/stylesheet-syntax.html#sub-controls。这是解决方案:

import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QComboBox, QWidget, QVBoxLayout


class TestComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.default_stylesheet = ''' 
        QComboBox {
            color: black;
            selection-color: black;
            selection-background-color: rgba(0, 0, 0, 0);
            background-color: rgba(0, 0, 0, 0);
        }
        
        QComboBox QAbstractItemView {
            background-color: white;
            min-width: 150px;
        }
        
        QComboBox:!hover::down-arrow {
        color: rgba(0, 0, 0, 0);
        }
        '''

        self.setStyleSheet(self.default_stylesheet)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.widget = QWidget(self)
        self.setCentralWidget(self.widget)

        self.layout = QVBoxLayout(self.widget)

        self.combobox = TestComboBox()
        self.combobox.resize(180, 30)
        self.combobox.insertItem(0, "item 1")
        self.combobox.insertItem(0, "item 2")
        self.combobox.insertItem(0, "item 3")
        self.combobox.insertItem(0, "item 4")
        self.combobox.insertItem(0, "item 5")
        self.layout.addWidget(self.combobox)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setEffectEnabled(Qt.UIEffect.UI_AnimateCombo, False)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

I managed to fix the issue by reading through the documentation https://doc.qt.io/qt-5/stylesheet-syntax.html#sub-controls. Here's the solution:

import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QComboBox, QWidget, QVBoxLayout


class TestComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.default_stylesheet = ''' 
        QComboBox {
            color: black;
            selection-color: black;
            selection-background-color: rgba(0, 0, 0, 0);
            background-color: rgba(0, 0, 0, 0);
        }
        
        QComboBox QAbstractItemView {
            background-color: white;
            min-width: 150px;
        }
        
        QComboBox:!hover::down-arrow {
        color: rgba(0, 0, 0, 0);
        }
        '''

        self.setStyleSheet(self.default_stylesheet)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.widget = QWidget(self)
        self.setCentralWidget(self.widget)

        self.layout = QVBoxLayout(self.widget)

        self.combobox = TestComboBox()
        self.combobox.resize(180, 30)
        self.combobox.insertItem(0, "item 1")
        self.combobox.insertItem(0, "item 2")
        self.combobox.insertItem(0, "item 3")
        self.combobox.insertItem(0, "item 4")
        self.combobox.insertItem(0, "item 5")
        self.layout.addWidget(self.combobox)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setEffectEnabled(Qt.UIEffect.UI_AnimateCombo, False)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文