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