有三个点的slider
主要是在设置了起始和结束限定后,可以拖动三个点,用于获取一个区间内的两个相连的子区间。
比如我想到0.03到0.5之间的数据集进行分段拟合。我想微调起始端和结束端的偏移,又想设置分段点,这个控件一次性可以设置这三个量。
![]()
from PySide6.QtWidgets import (
    QWidget,
    QApplication,
    QFrame,
    QVBoxLayout,
    QHBoxLayout,
    QLineEdit,
    QLabel,
)
from PySide6.QtCore import Qt, QRectF, QPointF, Signal, QMutex, QMutexLocker, QObject
from PySide6.QtGui import (
    QFocusEvent,
    QPainter,
    QColor,
    QPaintEvent,
    QPainterPath,
    QMouseEvent,
    QDoubleValidator,
)
import sys
from typing import Literal, Callable
from functools import wraps
class LineEdit(QLineEdit):
    sg_focusIn = Signal(str)
    sg_focusOut = Signal(str)
    def focusInEvent(self, arg__1: QFocusEvent) -> None:
        self.sg_focusIn.emit(self.text())
        return super().focusInEvent(arg__1)
    def focusOutEvent(self, arg__1: QFocusEvent) -> None:
        self.sg_focusOut.emit(self.text())
        return super().focusOutEvent(arg__1)
    def cnt(self, fn: Callable):
        self.textChanged.connect(fn)
        return self
class _msbase(QWidget):
    sg_changed = Signal(float, float, float, float, float)
    # ===
    class _field(QObject):
        sg_changed = Signal(float)
        _value: float = 0.0
        name: str = ""
        rec: QRectF
        old_str: str = ""
        old_value = 0.0
        ui: LineEdit = None  # type: ignore
        def __init__(self, name: str = ""):
            super().__init__()
            self.name = name
            self.vdt = _msbase.vld(self)
            self.sg_changed.connect(self.set_text)
            self.rec = QRectF()
        @property
        def value(self):
            return self._value
        @value.setter
        def value(self, v: float):
            self._value = float(v)
            self.sg_changed.emit(v)
        def setValue(self, v: float):
            self._value = float(v)
        def setlimt(
            self, f1: "_msbase._field|None" = None, f2: "_msbase._field|None" = None
        ):
            self.low = f1
            self.high = f2
            return self
        def validate(self, s: str) -> bool:
            r = None
            try:
                r = float(s)
            except:
                pass
            if r is None:
                return False
            else:
                if self.low is not None:
                    if r < self.low._value:
                        return False
                if self.high is not None:
                    if r > self.high._value:
                        return False
            return True
        def on_ui_focus_in(self, s):
            self.old_str = str(s)
            self.old_value = self._value
        def on_ui_focus_out(self, s=""):
            r = self.validate(self.ui.text())
            if not r:
                rr = self.fixup(self.old_value)
                self._value = self.old_value
                self.ui.setText(f"{rr:.4f}")
                self.ui.update()
        def on_ui_textchange(self, s):
            if self.validate(s):
                self._value = float(s)
        def fixup(self, f: float):
            r = f
            if self.low is not None:
                if f < self.low.value:
                    r = self.low.value
            if self.high is not None:
                if r > self.high.value:
                    r = self.high.value
            return r
        def __sub__(self, s: "_msbase._field"):
            if isinstance(s, _msbase._field):
                return self.value - s.value
            else:
                raise NotImplemented
        def __radd__(self, s: LineEdit) -> LineEdit:
            assert self.ui is None
            assert isinstance(s, LineEdit)
            self.ui = s
            s.sg_focusIn.connect(self.on_ui_focus_in)
            s.sg_focusOut.connect(self.on_ui_focus_out)
            s.returnPressed.connect(self.on_ui_focus_out)
            s.textChanged.connect(self.on_ui_textchange)
            self.sg_changed.connect(self.update_ui)
            self.ui.setValidator(self.vdt)
            self.ui.setText(f"")
            return s
        def update_ui(self, s):
            self.ui.update()
        @classmethod
        def sort(cls, *args: "_msbase._field"):
            ss: list[_msbase._field | None] = list(args)
            ss.append(None)
            ss.insert(0, None)
            for i in range(1, len(ss) - 1):
                s = ss[i]
                if s is not None:
                    s.setlimt(ss[i - 1], ss[i + 1])
        def set_text(self, f=0.0):
            if self.ui:
                self.ui.setText(f"{self.value:.4f}")
    class vld(QDoubleValidator):
        def __init__(self, f: "_msbase._field"):
            super().__init__()
            self.field = f
            self.setNotation(self.Notation.StandardNotation)
        def validate(self, s: str, p: int) -> object:
            return (
                (
                    self.State.Acceptable
                    if self.field.validate(s)
                    else self.State.Intermediate
                ),
                s,
                p,
            )
    # ===
    class _MSData(QObject):
        sg_draged = Signal(object)  # self
        sg_valueChanged = Signal(object)  # self
        def __init__(self) -> None:
            super().__init__()
            self.s = _msbase._field("s")
            self.a = _msbase._field("a")
            self.b = _msbase._field("b")
            self.c = _msbase._field("c")
            self.e = _msbase._field("e")
            _msbase._field.sort(self.s, self.a, self.b, self.c, self.e)
            self._mt = QMutex()
        def __str__(self):
            return str(
                (self.s.value, self.a.value, self.b.value, self.c.value, self.e.value)
            )
        __repr__ = __str__
        @property
        def isOk(self):
            return (
                self.s.value
                <= self.a.value
                <= self.b.value
                <= self.c.value
                <= self.e.value
            )
        @property
        def distance(self):
            ret = self.e - self.s
            if abs(ret) <= 1e-6:
                return 0
            else:
                return ret
        @property
        def a_pct(self):
            if self.distance == 0:
                return 0
            else:
                return (self.a - self.s) / self.distance
        @property
        def b_pct(self):
            if self.distance == 0:
                return 0
            else:
                return (self.b - self.s) / self.distance
        @property
        def c_pct(self):
            if self.distance == 0:
                return 0
            else:
                return (self.c - self.s) / self.distance
        def refresh_ui(self):
            self.sg_valueChanged.emit(self)
        def update_data(self):
            self.sg_draged.emit(self)
        def totuple(self):
            return self.s.value, self.a.value, self.b.value, self.c.value, self.e.value
    class SliderWidget(QFrame):
        def __init__(self, parent=None, data: "_msbase._MSData| None" = None):
            super().__init__(parent)
            self.s = 5
            self.setMinimumSize(200, self.s * 3)
            if data:
                self._model = data
            else:
                self._model = _msbase._MSData()
            self._model.sg_valueChanged.connect(self.on_value)
            self.data_long: float = 0
            self.old_data: float = 0
            self.old_x: float = 0
            self.dragging = None
            self.bar_start_x = 0
            self.bar_end_x = 0
        def paintEvent(self, event: QPaintEvent):
            rc = self.contentsRect()
            offset = 5
            r = self.s
            """circle radius"""
            x1 = rc.left() + offset + r
            """start point x"""
            x2 = rc.right() - offset - r
            """end point x"""
            datalong = x2 - x1 - 8 * r
            """数据段总长的计算基准"""
            if datalong < 0:
                datalong = 0
                x2 = x1 + datalong + 8 * r
            self.data_long = datalong
            y1 = rc.top()
            y2 = rc.bottom()
            if y2 - y1 < 2 * r + 1:
                y2 = y1 + 2 * r + 1
            painter = QPainter(self)
            content_high = 2 * r + 1
            y = int((y1 + y2) / 2 - content_high / 2) + r
            """起点 中心 y"""
            xs, ys = x1, y
            if self.data_long == 0 or self._model.distance == 0:
                aoffset = boffset = coffset = 0
            else:
                aoffset = min(max(datalong * self._model.a_pct, 0), datalong)
                _dis = (self._model.b_pct - self._model.a_pct) * datalong
                boffset = min(max(_dis, 0), datalong - aoffset) + aoffset
                _dis = (self._model.c_pct - self._model.b_pct) * datalong
                coffset = min(max(_dis, 0), datalong - boffset) + boffset
            groove_rect = QRectF(xs - r, ys - r, datalong + 10 * r, 2 * r + 1)
            painter.setPen(Qt.PenStyle.NoPen)
            painter.setBrush(QColor("#A0A0A0A0"))
            painter.drawRoundedRect(groove_rect, r, r)
            painter.setPen(QColor("red"))
            painter.setBrush(QColor("#A0ff2020"))
            mark = QPainterPath()
            _abc = [aoffset, boffset, coffset]
            _pabc = []
            for i in range(3):
                ax = x1 + r * (2 * i + 1) + _abc[i]
                ra = QRectF(ax, ys - r, r * 2 + 1, r * 2 + 1)
                _pabc.append(ra)
                mark.addRoundedRect(ra, 2, 2)
            with QMutexLocker(self._model._mt):
                self._model.a.rec = _pabc[0]
                self._model.b.rec = _pabc[1]
                self._model.c.rec = _pabc[2]
            painter.drawPath(mark)
        def mousePressEvent(self, event: QMouseEvent):
            if event.button() is Qt.MouseButton.LeftButton:
                pos = event.position()
                # 检查是否点击了A/B/C
                c: QRectF
                dt = self._model
                if (c := dt.c.rec) and c.contains(pos):
                    self.dragging = "C"
                    self.old_data = dt.c.value
                    self.old_x = event.position().x()
                elif (c := dt.b.rec) and c.contains(pos):
                    self.dragging = "B"
                    self.old_data = dt.b.value
                    self.old_x = event.position().x()
                elif (c := dt.a.rec) and c.contains(pos):
                    self.dragging = "A"
                    self.old_data = dt.a.value
                    self.old_x = event.position().x()
                else:
                    self.dragging = None
        def mouseMoveEvent(self, event: QMouseEvent):
            if not self.dragging:
                return
            if self.data_long <= 0 or self._model.distance <= 0:
                return
            dt = self._model
            dlt = (
                (event.position().x() - self.old_x)
                * self._model.distance
                / self.data_long
            )
            _idt = {
                "A": [dt.s.value - self.old_data, dt.b.value - self.old_data],
                "B": [dt.a.value - self.old_data, dt.c.value - self.old_data],
                "C": [dt.b.value - self.old_data, dt.e.value - self.old_data],
            }
            # 根据拖动对象处理限制
            if self.dragging in _idt:
                s, e = _idt[self.dragging]
                dlt = max(s, min(dlt, e))
            else:
                dlt = 0
            if dlt != 0:
                with QMutexLocker(dt._mt):
                    match self.dragging:
                        case "A":
                            dt.a.value = self.old_data + dlt
                        case "B":
                            dt.b.value = self.old_data + dlt
                        case "C":
                            dt.c.value = self.old_data + dlt
                dt.sg_draged.emit(dt)
                self.update()
        def mouseReleaseEvent(self, event):
            self.dragging = None
            self.old_data = 0
            self.old_x = 0
        def on_value(self, dt: "_msbase._MSData"):
            self.update()
class MutiSlider(_msbase):
    sg_changed = Signal(float, float, float, float, float)
    def __init__(self):
        super().__init__()
        self.float_length = 5
        self.data = self._MSData()
        self.start: float = 0.0
        self.end: float = 0.0
        self.data.sg_draged.connect(self._trg)
        self.data.sg_valueChanged.connect(self._trg)
    def setui(self):
        vbox = QVBoxLayout()
        vbox.addLayout(hb1 := QHBoxLayout())
        hb1.addWidget(QLabel("起始"))
        hb1.addWidget(LineEdit().cnt(self.on_v_changed) + self.data.s)
        hb1.addStretch(1)
        hb1.addWidget(QLabel("结束"))
        hb1.addWidget(LineEdit().cnt(self.on_v_changed) + self.data.e)
        self.sld = self.SliderWidget(data=self.data)
        vbox.addWidget(self.sld, 1)
        vbox.addLayout(hb2 := QHBoxLayout())
        hb2.addWidget(QLabel("A"))
        hb2.addWidget(LineEdit().cnt(self.on_v_changed) + self.data.a, 5)
        hb2.addStretch(1)
        hb2.addWidget(QLabel("B"))
        hb2.addWidget(LineEdit().cnt(self.on_v_changed) + self.data.b, 5)
        hb2.addStretch(1)
        hb2.addWidget(QLabel("C"))
        hb2.addWidget(LineEdit().cnt(self.on_v_changed) + self.data.c, 5)
        hb2.setContentsMargins(0, 0, 0, 0)
        hb2.setSpacing(1)
        self.setLayout(vbox)
        return self
    def setvalue(self, s: float, a: float, b: float, c: float, e: float):
        if s <= a <= b <= c <= e:
            dt = self.data
            with QMutexLocker(dt._mt):
                dt.s.setValue(s)
                dt.a.setValue(a)
                dt.b.setValue(b)
                dt.c.setValue(c)
                dt.e.setValue(e)
            dt.sg_valueChanged.emit(dt)
            dt.s.set_text()
            dt.a.set_text()
            dt.b.set_text()
            dt.c.set_text()
            dt.e.set_text()
    def getvalue(self):
        return self.data.totuple()
    def _trg(self, s: _msbase._MSData):
        self.sg_changed.emit(*s.totuple())
    def on_v_changed(self, v):
        self.data.sg_valueChanged.emit(self.data)
if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = MutiSlider().setui()
    widget.setvalue(0.03, 0.03, 0.13, 0.5, 0.5)
    widget.sg_changed.connect(print)
    widget.show()
    sys.exit(app.exec())