在图形用户界面(GUI)开发中,滑块控件是一种常用于选择数值范围的交互元素。然而,很多时候默认的滑块控件无法满足复杂的交互需求,例如同时选择一个范围的起始值和结束值。为此,实现了一个自定义的范围滑块控件------
QRangeSlider
,它允许用户通过拖动两个滑块来选择一个数值范围,并且支持动态显示当前值。
文章目录
- [1. 功能概述](#1. 功能概述)
- [2. 控件设计](#2. 控件设计)
-
- [2.1 主要属性](#2.1 主要属性)
- [2.2 信号](#2.2 信号)
- [2.3 方法](#2.3 方法)
- [3. 核心代码解析](#3. 核心代码解析)
-
- [3.1 绘制轨道和滑块](#3.1 绘制轨道和滑块)
- [3.2 滑块拖动逻辑](#3.2 滑块拖动逻辑)
- [3.3 值与位置的转换](#3.3 值与位置的转换)
- [4. 使用示例](#4. 使用示例)
- [5. 总结](#5. 总结)
1. 功能概述
QRangeSlider
是一个基于 PyQt5或PySide6 的自定义控件,它具有以下功能:
- 双滑块选择范围:用户可以通过拖动两个滑块来分别设置范围的起始值和结束值。
- 动态值显示:当滑块被拖动时,会动态显示当前滑块的值。
- 限制滑块范围:起始值不能超过结束值,结束值不能小于起始值,并且滑块不能移出轨道。
- 自定义外观:通过 PyQt 的绘图机制,可以轻松定制滑块和轨道的样式。
2. 控件设计
2.1 主要属性
start_value
和end_value
:分别表示范围的起始值和结束值。min_value
和max_value
:分别表示范围的最小值和最大值total_range
:表示整个滑块轨道的总范围,默认为 100。handle_radius
:滑块的半径,用于定义滑块的大小。is_dragging
:用于标记当前是否正在拖动滑块,以及拖动的是哪个滑块(start
或end
)。
2.2 信号
startValueChanged
:当起始值发生变化时发出的信号。endValueChanged
:当结束值发生变化时发出的信号。
2.3 方法
setRange(start, end, total_range=100)
:设置范围的起始值、结束值和总范围。paintEvent(event)
:重写绘图事件,用于绘制轨道和滑块。_value_to_position(value)
和_position_to_value(x)
:用于将值与像素位置相互转换。mousePressEvent(event)
、mouseMoveEvent(event)
和mouseReleaseEvent(event)
:处理鼠标事件,实现滑块的拖动功能。show_value_label(pos, value)
:动态显示滑块的值。
3. 核心代码解析
3.1 绘制轨道和滑块
python
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制轨道
painter.setPen(QPen(Qt.gray, 1))
painter.drawLine(self.handle_radius, self.height() // 2, self.width() - self.handle_radius, self.height() // 2)
# 绘制滑块
painter.setBrush(QBrush(QColor("#2AB8E5")))
painter.setPen(QPen(Qt.white, 0.5))
# 计算滑块位置
start_x = self._value_to_position(self.start_value)
end_x = self._value_to_position(self.end_value)
# 绘制圆形滑块
painter.drawEllipse(start_x - self.handle_radius, self.height() // 2 - self.handle_radius,
self.handle_radius * 2, self.handle_radius * 2)
painter.drawEllipse(end_x - self.handle_radius, self.height() // 2 - self.handle_radius,
self.handle_radius * 2, self.handle_radius * 2)
在 paintEvent
方法中,我们使用 QPainter
绘制了轨道和滑块。轨道是一条水平线,滑块是两个圆形,分别表示起始值和结束值。
3.2 滑块拖动逻辑
python
def mouseMoveEvent(self, event):
if self.is_dragging == "start":
new_value = self._position_to_value(event.position().toPoint().x())
# 限制 start_value 不能超过 end_value,且不能移出轨道
new_value = max(self.min_value, min(new_value, self.end_value))
if new_value != self.start_value:
self.start_value = new_value
self.update()
self.startValueChanged.emit(self.start_value)
self.show_value_label(event.position().toPoint(), self.start_value) # 显示值
elif self.is_dragging == "end":
new_value = self._position_to_value(event.position().toPoint().x())
# 限制 end_value 不能小于 start_value,且不能移出轨道
new_value = max(self.start_value, min(new_value, self.max_value))
if new_value != self.end_value:
self.end_value = new_value
self.update()
self.endValueChanged.emit(self.end_value)
self.show_value_label(event.position().toPoint(), self.end_value) # 显示值
在 mouseMoveEvent
方法中,我们根据鼠标的位置计算新的滑块值,并更新滑块的位置。同时,我们通过信号通知外部值的变化,并调用 show_value_label
方法动态显示当前值。
3.3 值与位置的转换
python
def _value_to_position(self, value):
"""将值转换为像素位置"""
return ((value - (-20)) / self.total_range) * (self.width() - 2 * self.handle_radius) + self.handle_radius
def _position_to_value(self, x):
"""将像素位置转换为值"""
return int(((x - self.handle_radius) / (self.width() - 2 * self.handle_radius)) * self.total_range) - 20
这两个方法用于将滑块的值与像素位置相互转换,确保滑块的值与位置之间的映射关系是正确的。
4. 使用示例
简单的使用示例:
python
import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
from PySide6.QtCore import Qt, Signal, QRect, QPoint
from PySide6.QtGui import QPainter, QPen, QBrush,QColor
class QRangeSlider(QWidget):
startValueChanged = Signal(int)
endValueChanged = Signal(int)
def __init__(self, parent=None):
super(QRangeSlider, self).__init__(parent)
self.setMinimumSize(200, 30) # 设置最小尺寸
self.start_value = 20
self.end_value = 80
self.min_value = -20
self.max_value = 150
self.total_range = 170
self.handle_radius = 10 # 滑块的半径
self.is_dragging = None # 当前拖动的滑块
# 创建 QLabel 用于显示值
self.label = QLabel(self)
self.label.setStyleSheet("color: white; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;")
self.label.hide() # 初始隐藏
def setRange(self, start, end, total_range=100):
self.start_value = start
self.end_value = end
self.total_range = total_range
self.update() # 更新绘制
self.startValueChanged.emit(self.start_value)
self.endValueChanged.emit(self.end_value)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制轨道
painter.setPen(QPen(Qt.gray, 1))
painter.drawLine(self.handle_radius, self.height() // 2, self.width() - self.handle_radius, self.height() // 2)
# 绘制滑块
painter.setBrush(QBrush(QColor("#2AB8E5")))
painter.setPen(QPen(Qt.white, 0.5))
# 计算滑块位置
start_x = self._value_to_position(self.start_value)
end_x = self._value_to_position(self.end_value)
# 绘制圆形滑块
painter.drawEllipse(start_x - self.handle_radius, self.height() // 2 - self.handle_radius,
self.handle_radius * 2, self.handle_radius * 2)
painter.drawEllipse(end_x - self.handle_radius, self.height() // 2 - self.handle_radius,
self.handle_radius * 2, self.handle_radius * 2)
def _value_to_position(self, value):
"""将值转换为像素位置"""
return ((value - (-20)) / self.total_range) * (self.width() - 2 * self.handle_radius) + self.handle_radius
def _position_to_value(self, x):
"""将像素位置转换为值"""
return int(((x - self.handle_radius) / (self.width() - 2 * self.handle_radius)) * self.total_range) - 20
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
start_x = self._value_to_position(self.start_value)
end_x = self._value_to_position(self.end_value)
# 判断点击的是哪个滑块
if abs(event.position().toPoint().x() - start_x) < self.handle_radius:
self.is_dragging = "start"
elif abs(event.position().toPoint().x() - end_x) < self.handle_radius:
self.is_dragging = "end"
else:
self.is_dragging = None
def mouseMoveEvent(self, event):
if self.is_dragging == "start":
new_value = self._position_to_value(event.position().toPoint().x())
# 限制 start_value 不能超过 end_value,且不能移出轨道
new_value = max(self.min_value, min(new_value, self.end_value))
if new_value != self.start_value:
self.start_value = new_value
self.update()
self.startValueChanged.emit(self.start_value)
self.show_value_label(event.position().toPoint(), self.start_value) # 显示值
elif self.is_dragging == "end":
new_value = self._position_to_value(event.position().toPoint().x())
# 限制 end_value 不能小于 start_value,且不能移出轨道
new_value = max(self.start_value, min(new_value, self.max_value))
if new_value != self.end_value:
self.end_value = new_value
self.update()
self.endValueChanged.emit(self.end_value)
self.show_value_label(event.position().toPoint(), self.end_value) # 显示值
def mouseReleaseEvent(self, event):
self.is_dragging = None
self.label.hide() # 隐藏值标签
def show_value_label(self, pos, value):
"""显示值标签,跟随鼠标位置,但 Y 轴固定在滑块中心"""
self.label.setText(str(value))
self.label.adjustSize() # 调整标签大小以适应内容
# 设置标签位置:X 轴跟随鼠标,Y 轴固定在滑块中心
label_x = pos.x() + self.handle_radius
label_y = (self.height() - self.label.height()) // 2 # 固定在滑块的垂直中心
# 边界检查:确保 label 不超出控件的右边界
label_x = min(label_x, self.width() - self.label.width()) # 留出一些间距
self.label.move(label_x, label_y)
self.label.show()
class DemoApp(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
layout = QVBoxLayout(self)
self.range_slider = QRangeSlider(self)
self.range_slider.setRange(20, 80, 170)
self.range_slider.startValueChanged.connect(self.on_start_value_changed)
self.range_slider.endValueChanged.connect(self.on_end_value_changed)
layout.addWidget(self.range_slider)
self.setWindowTitle("QRangeSlider Demo")
def on_start_value_changed(self, value):
print(f"Start value changed: {value}")
def on_end_value_changed(self, value):
print(f"End value changed: {value}")
if __name__ == "__main__":
app = QApplication([])
demo = DemoApp()
demo.show()
app.exec()
在这个示例中,创建了一个 QRangeSlider
控件,并设置了初始范围。当滑块的值发生变化时,会通过信号通知到外部,并打印当前值。
5. 总结
QRangeSlider
是一个功能强大且易于使用的自定义范围滑块控件。它通过 PySide6 的绘图机制和事件处理机制实现了双滑块选择范围的功能,并支持动态显示当前值。可以根据自己的需求进一步扩展和定制这个控件,例如添加更多的样式选项或支持更多的交互功能。