I found this customized dual-handle slider, I'd like to use it in my PyQt5 application, changing the styleSheet to match to my app style.
from PyQt5 import QtCore, QtGui, QtWidgets
class RangeSlider(QtWidgets.QSlider):
sliderMoved = QtCore.pyqtSignal(int, int)
""" A slider for ranges.
This class provides a dual-slider for ranges, where there is a defined
maximum and minimum, as is a normal slider, but instead of having a
single slider value, there are 2 slider values.
This class emits the same signals as the QSlider base class, with the
exception of valueChanged
"""
def __init__(self, *args):
super(RangeSlider, self).__init__(*args)
self._low = self.minimum()
self._high = self.maximum()
self.pressed_control = QtWidgets.QStyle.SC_None
self.tick_interval = 0
self.tick_position = QtWidgets.QSlider.NoTicks
self.hover_control = QtWidgets.QStyle.SC_None
self.click_offset = 0
# 0 for the low, 1 for the high, -1 for both
self.active_slider = 0
def low(self):
return self._low
def setLow(self, low: int):
self._low = low
self.update()
def high(self):
return self._high
def setHigh(self, high):
self._high = high
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
style = QtWidgets.QApplication.style()
# draw groove
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
opt.siderValue = 0
opt.sliderPosition = 0
opt.subControls = QtWidgets.QStyle.SC_SliderGroove
if self.tickPosition() != self.NoTicks:
opt.subControls |= QtWidgets.QStyle.SC_SliderTickmarks
style.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt, painter, self)
groove = style.subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderGroove, self)
self.initStyleOption(opt)
opt.subControls = QtWidgets.QStyle.SC_SliderGroove
opt.siderValue = 0
opt.sliderPosition = self._low
low_rect = style.subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self)
opt.sliderPosition = self._high
high_rect = style.subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self)
low_pos = self.__pick(low_rect.center())
high_pos = self.__pick(high_rect.center())
min_pos = min(low_pos, high_pos)
max_pos = max(low_pos, high_pos)
c = QtCore.QRect(low_rect.center(), high_rect.center()).center()
if opt.orientation == QtCore.Qt.Horizontal:
span_rect = QtCore.QRect(QtCore.QPoint(min_pos, c.y() - 2), QtCore.QPoint(max_pos, c.y() + 1))
else:
span_rect = QtCore.QRect(QtCore.QPoint(c.x() - 2, min_pos), QtCore.QPoint(c.x() + 1, max_pos))
if opt.orientation == QtCore.Qt.Horizontal:
groove.adjust(0, 0, -1, 0)
else:
groove.adjust(0, 0, 0, -1)
if True: # self.isEnabled():
highlight = self.palette().color(QtGui.QPalette.Highlight)
painter.setBrush(QtGui.QBrush(highlight))
painter.setPen(QtGui.QPen(highlight, 0))
'''
if opt.orientation == QtCore.Qt.Horizontal:
self.setupPainter(painter, opt.orientation, groove.center().x(), groove.top(), groove.center().x(), groove.bottom())
else:
self.setupPainter(painter, opt.orientation, groove.left(), groove.center().y(), groove.right(), groove.center().y())
'''
painter.drawRect(span_rect.intersected(groove))
for i, value in enumerate([self._low, self._high]):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
# Only draw the groove for the first slider, so it doesn't get drawn
# on top of the existing ones every time
if i == 0:
opt.subControls = QtWidgets.QStyle.SC_SliderHandle # | QtWidgets.QStyle.SC_SliderGroove
else:
opt.subControls = QtWidgets.QStyle.SC_SliderHandle
if self.tickPosition() != self.NoTicks:
opt.subControls |= QtWidgets.QStyle.SC_SliderTickmarks
if self.pressed_control:
opt.activeSubControls = self.pressed_control
else:
opt.activeSubControls = self.hover_control
opt.sliderPosition = value
opt.sliderValue = value
style.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt, painter, self)
def mousePressEvent(self, event):
event.accept()
style = QtWidgets.QApplication.style()
button = event.button()
# In a normal slider control, when the user clicks on a point in the
# slider's total range, but not on the slider part of the control the
# control would jump the slider value to where the user clicked.
# For this control, clicks which are not direct hits will slide both
# slider parts
if button:
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
self.active_slider = -1
for i, value in enumerate([self._low, self._high]):
opt.sliderPosition = value
hit = style.hitTestComplexControl(style.CC_Slider, opt, event.pos(), self)
if hit == style.SC_SliderHandle:
self.active_slider = i
self.pressed_control = hit
self.triggerAction(self.SliderMove)
self.setRepeatAction(self.SliderNoAction)
self.setSliderDown(True)
break
if self.active_slider < 0:
self.pressed_control = QtWidgets.QStyle.SC_SliderHandle
self.click_offset = self.__pixelPosToRangeValue(self.__pick(event.pos()))
self.triggerAction(self.SliderMove)
self.setRepeatAction(self.SliderNoAction)
else:
event.ignore()
def mouseMoveEvent(self, event):
if self.pressed_control != QtWidgets.QStyle.SC_SliderHandle:
event.ignore()
return
event.accept()
new_pos = self.__pixelPosToRangeValue(self.__pick(event.pos()))
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
if self.active_slider < 0:
offset = new_pos - self.click_offset
self._high += offset
self._low += offset
if self._low < self.minimum():
diff = self.minimum() - self._low
self._low += diff
self._high += diff
if self._high > self.maximum():
diff = self.maximum() - self._high
self._low += diff
self._high += diff
elif self.active_slider == 0:
if new_pos >= self._high:
new_pos = self._high - 1
self._low = new_pos
else:
if new_pos <= self._low:
new_pos = self._low + 1
self._high = new_pos
self.click_offset = new_pos
self.update()
self.sliderMoved.emit(self._low, self._high)
def __pick(self, pt):
if self.orientation() == QtCore.Qt.Horizontal:
return pt.x()
else:
return pt.y()
def __pixelPosToRangeValue(self, pos):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
style = QtWidgets.QApplication.style()
gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self)
sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self)
if self.orientation() == QtCore.Qt.Horizontal:
slider_length = sr.width()
slider_min = gr.x()
slider_max = gr.right() - slider_length + 1
else:
slider_length = sr.height()
slider_min = gr.y()
slider_max = gr.bottom() - slider_length + 1
return style.sliderValueFromPosition(self.minimum(), self.maximum(),
pos - slider_min, slider_max - slider_min,
opt.upsideDown)
import sys, os
from PyQt5 import QtCore, QtGui, QtWidgets
def echo(low_value, high_value):
print(low_value, high_value)
def main(argv):
app = QtWidgets.QApplication(sys.argv)
slider = RangeSlider(QtCore.Qt.Horizontal)
slider.setMinimumHeight(30)
slider.setMinimum(0)
slider.setMaximum(255)
slider.setLow(15)
slider.setHigh(35)
slider.setTickPosition(QtWidgets.QSlider.NoTicks)
slider.sliderMoved.connect(echo)
slider.setStyleSheet("QSlider::groove:horizontal {\n"
" border-radius: 5px;\n"
" height: 10px;\n"
" margin: 0px;\n"
" background-color: rgb(52, 59, 72);}\n"
"QSlider::groove:horizontal:hover {\n"
" background-color: rgb(55, 62, 76);\n}\n"
"QSlider::handle:horizontal {\n"
" background-color:#ff5555;\n"
" border: none;\n"
" height: 10px;\n"
" width: 10px;\n"
" margin: 0px;\n "
" border-radius: 5px;}\n"
"QSlider::handle:horizontal:hover {\n"
" background-color: rgb(195, 155, 255);}\n"
"QSlider::handle:horizontal:pressed {\n"
" background-color: rgb(255, 121, 198);}\n"
"QSlider::groove:vertical {\n"
" border-radius: 5px;\n"
" width: 10px;\n"
" margin: 0px;\n"
" background-color: rgb(52, 59, 72);}\n"
"QSlider::groove:vertical:hover {\n"
" background-color: rgb(55, 62, 76);}\n"
"QSlider::handle:vertical {\n"
" background-color:#ff5555;\n"
" border: none;\n"
" height: 10px;\b"
" width: 10px;\n"
" margin: 0px;\n"
" border-radius: 5px;}\n"
"QSlider::handle:vertical:hover {\n"
" background-color: rgb(195, 155, 255);}\n"
"QSlider::handle:vertical:pressed {\n"
" background-color: rgb(255, 121, 198);}")
slider.show()
slider.raise_()
app.exec_()
if __name__ == "__main__":
main(sys.argv)
As you can see, I've tried to change the styleSheet of the slider using slider.setStyleSheet(...) command, passing the style I'd like to apply. But it hasn't work.
I think, to achieve the results I want, I have to implement a setStyleSheet from scratch, changing the slider style after creating it.
Am I right, or is there a easy way to do it?
Get the style
from the widget, not from the application:
def paintEvent(self, event):
...
style = self.style()
...
Note that this has to be done for all style
references used in other functions, including mouse handlers.
The problem is that the painting of the slider relies on the style of the application, but in the code the stylesheet is applied to the widget.
When using setStyleSheet()
, Qt uses an internal QStyle subclass (QStyleSheetStyle) which is set based on what object setStyleSheet()
was called upon, but when a style sheet is set on a widget, only the style of that widget is changed (eventually propagating to its children if the stylesheet syntax requires it).
The following line of paintEvent()
uses the wrong style:
style = QtWidgets.QApplication.style()
The application style has no knowledge of the style sheet set on the widget, so it behaves like there is no style sheet set at all. Add the following line near the end and you'll see that those styles are not the same:
print(app.style() is slider.style())
In fact, if you change the setStyleSheet()
line to the following, it will work as expected:
app.setStyleSheet("QSlider::groove:horizontal {\n"
...
)
In reality, it is the style reference used in the painting that is wrong, so the important change to do is the following:
def paintEvent(self, event):
painter = QtGui.QPainter(self)
style = self.style() # <-- always use the widget style!
...
That reference change is important, and it's what calls to the QStyle functions rely upon.
If you look at the code in paintEvent()
, you'll see that there are a few calls to the style
object functions (drawComplexControl
, subControlRect
, etc.) that also have self
as final argument. QStyle uses the widget to know how to behave depending on the widget, but still based on the style rules.
If the used style is that of the widget (aka, the QStyleSheetStyle based on its style sheet), it knows how to properly paint the custom slider, since the style has its rules set from the style sheet syntax. But if you use the application style (which has no stylesheet set), it knows nothing about that widget's stylesheet, so it will just paint it using the default behavior.
Note that the above is also important for any QStyle function that relies on style sheet rules: for example, for mouse handlers, otherwise Qt will use wrong coordinates (based on the wrong style) to get the control positions like those returned by subControlRect()
.
Still, it is correct (and normally recommended) to set an application wide stylesheet: it's simpler for code maintenance, and can also partially improve memory performance, since setting a global stylesheet uses a unique inherited QStyleSheetStyle, while setting individual style sheets to different widgets creates separate instances of that style for each one of those widgets (even if their style sheets are identical!).
You may still not want to change the appearance of any slider, though: maybe you want to keep showing normal sliders with the default appearance.
In that case you should consider using proper selector types; for situations like these, a type selector is probably the best choice, since it will be applied to all instances of the custom slider. Any reference to QSlider
in the stylesheet should then be changed to the class name of the custom slider:
app.setStyleSheet("RangeSlider::groove:horizontal {\n"
... # etc.
)
Note that the above doesn't change the fact that the widget must always use its own style, even if an application style sheet was set. For example, you may need to set a specialized stylesheet for a single instance of your slider, using slider.setStyleSheet()
for simplicity: if you still use the application style, you'll end up with the same issue. So, always use the widget style.
Note that your style sheet has an invalid character near the end: there is a \b
(which is the ASCII Backspace character) instead of a \n
(line break):
"QSlider::handle:vertical {\n"
" background-color:#ff5555;\n"
" border: none;\n"
" height: 10px;\b"
^^^ change to \n
This didn't actually affect your example because the invalid character was after all definitions for horizontal sliders, but it would have affected any vertical sliders because it would have made the CSS invalid from that point on. Similarly to web browsers, Qt considers the CSS syntax (and what it does) until an error occurs, but, from that point on, any further line is ignored.
Your IDE probably didn't show it, but the error exists and is visible when running the original code from a terminal or prompt, so always remember to test your codes outside the IDE:
>>> StdErr: Could not parse application stylesheet
Also note that, for better code readability, you should avoid using that string syntax (you probably got it from a pyuic generated file). In fact, the CSS syntax (on which QSS are based) can avoid line breaks and basically ignores any space between definitions (the semicolon is the only important separator between properties), similarly to real browsers.
You get that syntax because that's how it's taken from the UI file (spaces and line breaks remain consistent in the CSS editors), but for hand-written QSS it's just annoying and quite unreadable.
If you want, you can keep the multi-line strings and remove any new line characters, but an easier and more readable approach is to use triple quotes and write the QSS in a single block, without distracting characters:
app.setStyleSheet('''
RangeSlider::groove:horizontal {
border-radius: 5px;
height: 10px;
margin: 0px;
background-color: rgb(52, 59, 72);
}
RangeSlider::groove:horizontal:hover {
background-color: rgb(55, 62, 76);
}
... etc.
''')