将范围滑块小部件移植到 PyQt5
Posted
技术标签:
【中文标题】将范围滑块小部件移植到 PyQt5【英文标题】:Porting range-slider widget to PyQt5 【发布时间】:2017-11-17 01:59:18 【问题描述】:我目前需要一个范围滑块(一个可以设置最小值和最大值的滑块)。我发现了两个相关的问题Range slider in Qt (two handles in a QSlider) 和Why RangeSlider is available in QtQuick and not as standard Widget,但它们都不是用 python3 编写的,而且我对 C++ 不是很熟悉。
我找到了这个完美的 github 工具 https://github.com/rsgalloway/qrangeslider,但不幸的是它是为 PyQt4 编写的,我正在使用 PyQt5。
我打算用 PyQt5 绑定重新格式化这个 github 源,但在这样做之前我想知道是否有人以前做过这样我可以节省时间? 或者,如果有人有不同的解决方案,我愿意接受建议。
【问题讨论】:
【参考方案1】:下面是QRangeSlider widget 的 PyQt5 端口。为了简洁起见,我删除了所有 cmets、doc-strings、assert 语句等。它似乎在 Python 2 和 Python 3 上都可以正常工作,但我并没有真正对其进行过多测试。
qrangeslider.py:
import sys, os
from PyQt5 import QtCore, QtGui, QtWidgets
__all__ = ['QRangeSlider']
DEFAULT_CSS = """
QRangeSlider *
border: 0px;
padding: 0px;
QRangeSlider #Head
background: #222;
QRangeSlider #Span
background: #393;
QRangeSlider #Span:active
background: #282;
QRangeSlider #Tail
background: #222;
QRangeSlider > QSplitter::handle
background: #393;
QRangeSlider > QSplitter::handle:vertical
height: 4px;
QRangeSlider > QSplitter::handle:pressed
background: #ca5;
"""
def scale(val, src, dst):
return int(((val - src[0]) / float(src[1]-src[0])) * (dst[1]-dst[0]) + dst[0])
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("QRangeSlider")
Form.resize(300, 30)
Form.setStyleSheet(DEFAULT_CSS)
self.gridLayout = QtWidgets.QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0)
self.gridLayout.setObjectName("gridLayout")
self._splitter = QtWidgets.QSplitter(Form)
self._splitter.setMinimumSize(QtCore.QSize(0, 0))
self._splitter.setMaximumSize(QtCore.QSize(16777215, 16777215))
self._splitter.setOrientation(QtCore.Qt.Horizontal)
self._splitter.setObjectName("splitter")
self._head = QtWidgets.QGroupBox(self._splitter)
self._head.setTitle("")
self._head.setObjectName("Head")
self._handle = QtWidgets.QGroupBox(self._splitter)
self._handle.setTitle("")
self._handle.setObjectName("Span")
self._tail = QtWidgets.QGroupBox(self._splitter)
self._tail.setTitle("")
self._tail.setObjectName("Tail")
self.gridLayout.addWidget(self._splitter, 0, 0, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("QRangeSlider", "QRangeSlider"))
class Element(QtWidgets.QGroupBox):
def __init__(self, parent, main):
super(Element, self).__init__(parent)
self.main = main
def setStyleSheet(self, style):
self.parent().setStyleSheet(style)
def textColor(self):
return getattr(self, '__textColor', QtGui.QColor(125, 125, 125))
def setTextColor(self, color):
if type(color) == tuple and len(color) == 3:
color = QtGui.QColor(color[0], color[1], color[2])
elif type(color) == int:
color = QtGui.QColor(color, color, color)
setattr(self, '__textColor', color)
def paintEvent(self, event):
qp = QtGui.QPainter()
qp.begin(self)
if self.main.drawValues():
self.drawText(event, qp)
qp.end()
class Head(Element):
def __init__(self, parent, main):
super(Head, self).__init__(parent, main)
def drawText(self, event, qp):
qp.setPen(self.textColor())
qp.setFont(QtGui.QFont('Arial', 10))
qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.min()))
class Tail(Element):
def __init__(self, parent, main):
super(Tail, self).__init__(parent, main)
def drawText(self, event, qp):
qp.setPen(self.textColor())
qp.setFont(QtGui.QFont('Arial', 10))
qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.max()))
class Handle(Element):
def __init__(self, parent, main):
super(Handle, self).__init__(parent, main)
def drawText(self, event, qp):
qp.setPen(self.textColor())
qp.setFont(QtGui.QFont('Arial', 10))
qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.start()))
qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.end()))
def mouseMoveEvent(self, event):
event.accept()
mx = event.globalX()
_mx = getattr(self, '__mx', None)
if not _mx:
setattr(self, '__mx', mx)
dx = 0
else:
dx = mx - _mx
setattr(self, '__mx', mx)
if dx == 0:
event.ignore()
return
elif dx > 0:
dx = 1
elif dx < 0:
dx = -1
s = self.main.start() + dx
e = self.main.end() + dx
if s >= self.main.min() and e <= self.main.max():
self.main.setRange(s, e)
class QRangeSlider(QtWidgets.QWidget, Ui_Form):
endValueChanged = QtCore.pyqtSignal(int)
maxValueChanged = QtCore.pyqtSignal(int)
minValueChanged = QtCore.pyqtSignal(int)
startValueChanged = QtCore.pyqtSignal(int)
minValueChanged = QtCore.pyqtSignal(int)
maxValueChanged = QtCore.pyqtSignal(int)
startValueChanged = QtCore.pyqtSignal(int)
endValueChanged = QtCore.pyqtSignal(int)
_SPLIT_START = 1
_SPLIT_END = 2
def __init__(self, parent=None):
super(QRangeSlider, self).__init__(parent)
self.setupUi(self)
self.setMouseTracking(False)
self._splitter.splitterMoved.connect(self._handleMoveSplitter)
self._head_layout = QtWidgets.QHBoxLayout()
self._head_layout.setSpacing(0)
self._head_layout.setContentsMargins(0, 0, 0, 0)
self._head.setLayout(self._head_layout)
self.head = Head(self._head, main=self)
self._head_layout.addWidget(self.head)
self._handle_layout = QtWidgets.QHBoxLayout()
self._handle_layout.setSpacing(0)
self._handle_layout.setContentsMargins(0, 0, 0, 0)
self._handle.setLayout(self._handle_layout)
self.handle = Handle(self._handle, main=self)
self.handle.setTextColor((150, 255, 150))
self._handle_layout.addWidget(self.handle)
self._tail_layout = QtWidgets.QHBoxLayout()
self._tail_layout.setSpacing(0)
self._tail_layout.setContentsMargins(0, 0, 0, 0)
self._tail.setLayout(self._tail_layout)
self.tail = Tail(self._tail, main=self)
self._tail_layout.addWidget(self.tail)
self.setMin(0)
self.setMax(99)
self.setStart(0)
self.setEnd(99)
self.setDrawValues(True)
def min(self):
return getattr(self, '__min', None)
def max(self):
return getattr(self, '__max', None)
def setMin(self, value):
setattr(self, '__min', value)
self.minValueChanged.emit(value)
def setMax(self, value):
setattr(self, '__max', value)
self.maxValueChanged.emit(value)
def start(self):
return getattr(self, '__start', None)
def end(self):
return getattr(self, '__end', None)
def _setStart(self, value):
setattr(self, '__start', value)
self.startValueChanged.emit(value)
def setStart(self, value):
v = self._valueToPos(value)
self._splitter.splitterMoved.disconnect()
self._splitter.moveSplitter(v, self._SPLIT_START)
self._splitter.splitterMoved.connect(self._handleMoveSplitter)
self._setStart(value)
def _setEnd(self, value):
setattr(self, '__end', value)
self.endValueChanged.emit(value)
def setEnd(self, value):
v = self._valueToPos(value)
self._splitter.splitterMoved.disconnect()
self._splitter.moveSplitter(v, self._SPLIT_END)
self._splitter.splitterMoved.connect(self._handleMoveSplitter)
self._setEnd(value)
def drawValues(self):
return getattr(self, '__drawValues', None)
def setDrawValues(self, draw):
setattr(self, '__drawValues', draw)
def getRange(self):
return (self.start(), self.end())
def setRange(self, start, end):
self.setStart(start)
self.setEnd(end)
def keyPressEvent(self, event):
key = event.key()
if key == QtCore.Qt.Key_Left:
s = self.start()-1
e = self.end()-1
elif key == QtCore.Qt.Key_Right:
s = self.start()+1
e = self.end()+1
else:
event.ignore()
return
event.accept()
if s >= self.min() and e <= self.max():
self.setRange(s, e)
def setBackgroundStyle(self, style):
self._tail.setStyleSheet(style)
self._head.setStyleSheet(style)
def setSpanStyle(self, style):
self._handle.setStyleSheet(style)
def _valueToPos(self, value):
return scale(value, (self.min(), self.max()), (0, self.width()))
def _posToValue(self, xpos):
return scale(xpos, (0, self.width()), (self.min(), self.max()))
def _handleMoveSplitter(self, xpos, index):
hw = self._splitter.handleWidth()
def _lockWidth(widget):
width = widget.size().width()
widget.setMinimumWidth(width)
widget.setMaximumWidth(width)
def _unlockWidth(widget):
widget.setMinimumWidth(0)
widget.setMaximumWidth(16777215)
v = self._posToValue(xpos)
if index == self._SPLIT_START:
_lockWidth(self._tail)
if v >= self.end():
return
offset = -20
w = xpos + offset
self._setStart(v)
elif index == self._SPLIT_END:
_lockWidth(self._head)
if v <= self.start():
return
offset = -40
w = self.width() - xpos + offset
self._setEnd(v)
_unlockWidth(self._tail)
_unlockWidth(self._head)
_unlockWidth(self._handle)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
rs = QRangeSlider()
rs.show()
rs.setRange(15, 35)
rs.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
rs.handle.setStyleSheet('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);')
app.exec_()
如果要运行examples,只需更改以下代码块(在文件顶部):
examples.py:
import sys, os
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from qrangeslider import QRangeSlider
app = QtWidgets.QApplication(sys.argv)
...
【讨论】:
@mangovn 请参阅 PyQt 文档中的示例:Using Qt Designer。一旦你像这样设置了主窗口,你就可以添加你喜欢的任何小部件。或者,您可以使用小部件提升通过 Qt 设计器添加它(参见 here 和 here)。【参考方案2】:这是一个具有原生外观的 PySide2 示例:
from PySide2.QtWidgets import *
from PySide2.QtCore import *
from PySide2.QtGui import *
import sys
class RangeSlider(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.first_position = 1
self.second_position = 8
self.opt = QStyleOptionSlider()
self.opt.minimum = 0
self.opt.maximum = 10
self.setTickPosition(QSlider.TicksAbove)
self.setTickInterval(1)
self.setSizePolicy(
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed, QSizePolicy.Slider)
)
def setRangeLimit(self, minimum: int, maximum: int):
self.opt.minimum = minimum
self.opt.maximum = maximum
def setRange(self, start: int, end: int):
self.first_position = start
self.second_position = end
def getRange(self):
return (self.first_position, self.second_position)
def setTickPosition(self, position: QSlider.TickPosition):
self.opt.tickPosition = position
def setTickInterval(self, ti: int):
self.opt.tickInterval = ti
def paintEvent(self, event: QPaintEvent):
painter = QPainter(self)
# Draw rule
self.opt.initFrom(self)
self.opt.rect = self.rect()
self.opt.sliderPosition = 0
self.opt.subControls = QStyle.SC_SliderGroove | QStyle.SC_SliderTickmarks
# Draw GROOVE
self.style().drawComplexControl(QStyle.CC_Slider, self.opt, painter)
# Draw INTERVAL
color = self.palette().color(QPalette.Highlight)
color.setAlpha(160)
painter.setBrush(QBrush(color))
painter.setPen(Qt.NoPen)
self.opt.sliderPosition = self.first_position
x_left_handle = (
self.style()
.subControlRect(QStyle.CC_Slider, self.opt, QStyle.SC_SliderHandle)
.right()
)
self.opt.sliderPosition = self.second_position
x_right_handle = (
self.style()
.subControlRect(QStyle.CC_Slider, self.opt, QStyle.SC_SliderHandle)
.left()
)
groove_rect = self.style().subControlRect(
QStyle.CC_Slider, self.opt, QStyle.SC_SliderGroove
)
selection = QRect(
x_left_handle,
groove_rect.y(),
x_right_handle - x_left_handle,
groove_rect.height(),
).adjusted(-1, 1, 1, -1)
painter.drawRect(selection)
# Draw first handle
self.opt.subControls = QStyle.SC_SliderHandle
self.opt.sliderPosition = self.first_position
self.style().drawComplexControl(QStyle.CC_Slider, self.opt, painter)
# Draw second handle
self.opt.sliderPosition = self.second_position
self.style().drawComplexControl(QStyle.CC_Slider, self.opt, painter)
def mousePressEvent(self, event: QMouseEvent):
self.opt.sliderPosition = self.first_position
self._first_sc = self.style().hitTestComplexControl(
QStyle.CC_Slider, self.opt, event.pos(), self
)
self.opt.sliderPosition = self.second_position
self._second_sc = self.style().hitTestComplexControl(
QStyle.CC_Slider, self.opt, event.pos(), self
)
def mouseMoveEvent(self, event: QMouseEvent):
distance = self.opt.maximum - self.opt.minimum
pos = self.style().sliderValueFromPosition(
0, distance, event.pos().x(), self.rect().width()
)
if self._first_sc == QStyle.SC_SliderHandle:
if pos <= self.second_position:
self.first_position = pos
self.update()
return
if self._second_sc == QStyle.SC_SliderHandle:
if pos >= self.first_position:
self.second_position = pos
self.update()
def sizeHint(self):
""" override """
SliderLength = 84
TickSpace = 5
w = SliderLength
h = self.style().pixelMetric(QStyle.PM_SliderThickness, self.opt, self)
if (
self.opt.tickPosition & QSlider.TicksAbove
or self.opt.tickPosition & QSlider.TicksBelow
):
h += TickSpace
return (
self.style()
.sizeFromContents(QStyle.CT_Slider, self.opt, QSize(w, h), self)
.expandedTo(QApplication.globalStrut())
)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = RangeSlider()
w.show()
# q = QSlider()
# q.show()
app.exec_()
【讨论】:
这个类可以从 QSlider 继承来提供像 valuechanged 这样的信号 我怎样才能让它垂直?【参考方案3】:据我所知,@ekhumoro 的实现存在一个错误,即当您尝试将滑块拖动到其上限时,由于滑块宽度,比例限制设置错误。
我通过更改以下内容来修复它;
v = self._posToValue(xpos)
到;
if index == 1:
v = self._posToValue(xpos)
elif index == 2:
v = self._posToValue(xpos+hw)
【讨论】:
以上是关于将范围滑块小部件移植到 PyQt5的主要内容,如果未能解决你的问题,请参考以下文章
将 Slider 小部件中的值传递到 firestore 数据库