自定义双柄QSlider样式表失效:如何适配PyQt5应用风格?
问题描述
我找到了一款自定义双柄滑块,希望在PyQt5应用中使用它,并修改其StyleSheet以适配应用风格。尝试通过slider.setStyleSheet(...)传入目标样式,但未生效,疑惑是否需要从头实现样式设置逻辑,还是存在更简便的修改方式。
附原代码:
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)) 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() 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)
问题原因与解决方法
核心问题
这个自定义RangeSlider的paintEvent是手动绘制的,存在三个关键问题导致StyleSheet失效:
- 中间高亮区域直接调用
palette().color(QPalette.Highlight)绘制,完全忽略StyleSheet配置 - 绘制滑块手柄时,未正确传递hover、pressed等状态到
QStyleOptionSlider,导致StyleSheet的状态伪类无法生效 - 原代码存在笔误:
opt.siderValue应为opt.sliderValue,导致样式解析异常
修复步骤
1. 修复代码笔误
在paintEvent中,将两处opt.siderValue = 0修改为opt.sliderValue = 0
2. 让中间高亮区域支持StyleSheet
替换原手动绘制高亮区域的代码,改用Qt内置的SC_SliderSubPage子控件,自动适配StyleSheet:
# 替换原高亮绘制代码 opt_sub = QtWidgets.QStyleOptionSlider(opt) opt_sub.sliderPosition = self._low opt_sub.sliderValue = self._low opt_sub.subControls = QtWidgets.QStyle.SC_SliderSubPage style.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt_sub, painter, self)
3. 正确传递滑块状态到StyleOption
在绘制两个手柄时,添加状态判断,将hover、pressed状态注入QStyleOptionSlider:
for i, value in enumerate([self._low, self._high]): opt = QtWidgets.QStyleOptionSlider() self.initStyleOption(opt) opt.subControls = QtWidgets.QStyle.SC_SliderHandle if self.tickPosition() != self.NoTicks: opt.subControls |= QtWidgets.QStyle.SC_SliderTickmarks # 注入状态,适配StyleSheet伪类 if self.pressed_control == QtWidgets.QStyle.SC_SliderHandle and self.active_slider == i: opt.state |= QtWidgets.QStyle.State_Sunken elif self.hover_control == QtWidgets.QStyle.SC_SliderHandle and self.active_slider == i: opt.state |= QtWidgets.QStyle.State_MouseOver opt.sliderPosition = value opt.sliderValue = value style.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt, painter, self)
4. 修正并调整StyleSheet
修复原StyleSheet中的笔误(将\b改为\n),并添加sub-page样式控制中间高亮区域:
QSlider::groove:horizontal { border-radius: 5px; height: 10px; margin: 0px; background-color: rgb(52, 59, 72); } QSlider::groove:horizontal:hover { background-color: rgb(55, 62, 76); } QSlider::sub-page:horizontal { background-color: #ff5555; border-radius: 5px; } QSlider::handle:horizontal { background-color:#ffffff; border: none; height: 10px; width: 10px; margin: 0px; border-radius: 5px; } QSlider::handle:horizontal:hover { background-color: rgb(195, 155, 255); } QSlider::handle:horizontal:pressed { background-color: rgb(255, 121, 198); } /* 垂直滑块样式可参照上述格式调整 */
将上述修改整合到原代码后,StyleSheet即可正常生效,无需从头实现样式逻辑。
内容的提问来源于stack exchange,提问作者Igor
相关产品推荐
相关产品推荐

