python GUI之实现一个自定义的范围滑块控件:QRangeSlider

在图形用户界面(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_valueend_value:分别表示范围的起始值和结束值。
  • min_valuemax_value:分别表示范围的最小值和最大值
  • total_range:表示整个滑块轨道的总范围,默认为 100。
  • handle_radius:滑块的半径,用于定义滑块的大小。
  • is_dragging:用于标记当前是否正在拖动滑块,以及拖动的是哪个滑块(startend)。

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 的绘图机制和事件处理机制实现了双滑块选择范围的功能,并支持动态显示当前值。可以根据自己的需求进一步扩展和定制这个控件,例如添加更多的样式选项或支持更多的交互功能。

相关推荐
我不会编程5553 小时前
Python Cookbook-2.24 在 Mac OSX平台上统计PDF文档的页数
开发语言·python·pdf
胡歌14 小时前
final 关键字在不同上下文中的用法及其名称
开发语言·jvm·python
程序员张小厨4 小时前
【0005】Python变量详解
开发语言·python
Hacker_Oldv5 小时前
Python 爬虫与网络安全有什么关系
爬虫·python·web安全
深蓝海拓5 小时前
PySide(PyQT)重新定义contextMenuEvent()实现鼠标右键弹出菜单
开发语言·python·pyqt
数据攻城小狮子7 小时前
深入剖析 OpenCV:全面掌握基础操作、图像处理算法与特征匹配
图像处理·python·opencv·算法·计算机视觉
ONE_PUNCH_Ge7 小时前
Python 爬虫 – BeautifulSoup
python
L_cl8 小时前
【Python 数据结构 1.零基础复习】
数据结构·python
Monkey_Jun8 小时前
《Python百练成仙》31-40章(不定时更新)
开发语言·python
没事偷着乐琅8 小时前
人工智能 pytorch篇
人工智能·pytorch·python