背景:
PySide2 与 qt5 有版本冲突,
# from superqt import QRangeSlider # 得QT6才行
from qtrangeslider import QRangeSlider 也不兼容
有不方便升级 PySide2
所以自实现写一个 完全 PySide2 5.15.2 兼容的双滑块控件,不依赖外部 qtrangeslider
,直接用 QWidget
+ QPainter
自绘两个滑块,支持水平模式、数值范围、信号触发。
效果如下:
实现:
自定义控件
(统一放到新建main_test.py文件中)
from PySide2.QtCore import Qt, QRect, Signal, QPoint
from PySide2.QtGui import QPainter, QColor, QPen, QBrush
from PySide2.QtWidgets import QWidget, QSizePolicy
class QRangeSlider(QWidget):
valueChanged = Signal(tuple) # (low, high)
def __init__(self, orientation=Qt.Horizontal, parent=None):
super().__init__(parent)
self.orientation = orientation
self._min = 0
self._max = 100
self._low_value = 20
self._high_value = 80
self._handle_radius = 8
self._bar_height = 4
self._active_handle = None
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setMinimumHeight(30)
# ------- Public API -------
def setRange(self, min_val, max_val):
self._min = min_val
self._max = max_val
self.update()
def setValue(self, values):
low, high = values
self._low_value = max(self._min, min(low, self._max))
self._high_value = max(self._min, min(high, self._max))
self.update()
self.valueChanged.emit((self._low_value, self._high_value))
def value(self):
return (self._low_value, self._high_value)
# ------- Qt Events -------
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 计算坐标
bar_rect = QRect(
self._handle_radius,
(self.height() - self._bar_height) // 2,
self.width() - self._handle_radius * 2,
self._bar_height
)
# 绘制背景条
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(200, 200, 200))
painter.drawRect(bar_rect)
# 绘制选中范围
low_x = self._value_to_pos(self._low_value)
high_x = self._value_to_pos(self._high_value)
selected_rect = QRect(low_x, bar_rect.y(), high_x - low_x, self._bar_height)
painter.setBrush(QColor(100, 180, 255))
painter.drawRect(selected_rect)
# 绘制两个滑块
painter.setBrush(QBrush(QColor(255, 100, 100)))
painter.drawEllipse(QPoint(low_x, self.height() // 2), self._handle_radius, self._handle_radius)
painter.setBrush(QBrush(QColor(100, 100, 255)))
painter.drawEllipse(QPoint(high_x, self.height() // 2), self._handle_radius, self._handle_radius)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
low_x = self._value_to_pos(self._low_value)
high_x = self._value_to_pos(self._high_value)
if abs(event.x() - low_x) < self._handle_radius + 2:
self._active_handle = "low"
elif abs(event.x() - high_x) < self._handle_radius + 2:
self._active_handle = "high"
def mouseMoveEvent(self, event):
if self._active_handle:
new_val = self._pos_to_value(event.x())
if self._active_handle == "low":
self._low_value = new_val
elif self._active_handle == "high":
self._high_value = new_val
self.update()
self.valueChanged.emit((self._low_value, self._high_value))
def mouseReleaseEvent(self, event):
if self._low_value > self._high_value:
# 交换,保证 low <= high
self._low_value, self._high_value = self._high_value, self._low_value
self._active_handle = None
self.update()
self.valueChanged.emit((self._low_value, self._high_value))
# ------- Utils -------
def _value_to_pos(self, value):
bar_start = self._handle_radius
bar_end = self.width() - self._handle_radius
ratio = (value - self._min) / (self._max - self._min)
return int(bar_start + ratio * (bar_end - bar_start))
def _pos_to_value(self, pos):
bar_start = self._handle_radius
bar_end = self.width() - self._handle_radius
ratio = (pos - bar_start) / (bar_end - bar_start)
ratio = max(0, min(1, ratio))
return int(self._min + ratio * (self._max - self._min))
测试:
在上述同一py文件下,添加:
if __name__ == "__main__":
from PySide2.QtWidgets import QApplication, QVBoxLayout, QWidget
import sys
app = QApplication(sys.argv)
win = QWidget()
layout = QVBoxLayout(win)
slider = QRangeSlider()
slider.setRange(0, 100)
slider.setValue((20, 80))
slider.valueChanged.connect(lambda v: print("当前值:", v))
layout.addWidget(slider)
win.show()
sys.exit(app.exec_())
布局
上述控件定义好后,如何在QT creater中使用呢?
可以采用占位符 + 替换的方式,替换成自己的控件
首先,先在ui中插入一个QWidget, 如下:
<widget class="QWidget" name="range_slider_placeholder" native="true">
<property name="geometry">
<rect>
<x>150</x>
<y>440</y>
<width>451</width>
<height>81</height>
</rect>
</property>
</widget>
然后代码替换:
placeholder = self.ui.findChild(QWidget, "range_slider_placeholder")
range_slider = QRangeSliderJ(Qt.Horizontal, self.ui)
range_slider.setRange(0, 100)
range_slider.setValue((20, 80))
replace_placeholder_with_widget(placeholder, range_slider)
替换函数的具体实现如下:
def replace_placeholder_with_widget(placeholder: QWidget, new_widget: QWidget):
"""
将 UI 中的占位符控件替换为新的控件,兼容:
- QGridLayout
- QHBoxLayout / QVBoxLayout
- 无布局的父控件(直接定位到占位符的位置)
:param placeholder: 原占位符 QWidget
:param new_widget: 替换用的新 QWidget
"""
if placeholder is None or placeholder.parent() is None:
raise ValueError("占位符无效或没有父控件")
parent = placeholder.parent()
layout = parent.layout()
if layout is None:
# 无布局:直接放到占位符的几何位置
new_widget.setParent(parent)
new_widget.setGeometry(placeholder.geometry())
elif isinstance(layout, QGridLayout):
index = layout.indexOf(placeholder)
if index != -1:
row, col, row_span, col_span = layout.getItemPosition(index)
layout.addWidget(new_widget, row, col, row_span, col_span)
layout.removeWidget(placeholder)
else:
layout.addWidget(new_widget)
else:
# QHBoxLayout / QVBoxLayout
index = layout.indexOf(placeholder)
if index != -1:
layout.insertWidget(index, new_widget)
layout.removeWidget(placeholder)
else:
layout.addWidget(new_widget)
placeholder.hide()
placeholder.deleteLater()
new_widget.show()