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
andbackground-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'spalette()
to get the colors ofHighlight
andHighlightedText
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 ofQComboBox
, 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()
(orself.setVisible(True)
) within the__init__
of a widget;
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.