[SOLVED] QComboBox has unexpected behavior when changing stylesheets while Qt.UIEffect.UI_AnimateCombo is set to False

Issue

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())

Solution

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;

Answered By – musicamante

Answer Checked By – Terry (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *