三点拖动条

有三个点的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())

posted @ 2025-05-31 14:10  方头狮  阅读(11)  评论(0)    收藏  举报